diff --git a/build.gradle b/build.gradle index e6d337163a..879ed159ee 100644 --- a/build.gradle +++ b/build.gradle @@ -51,7 +51,7 @@ buildscript { web3jVersion = '4.9.5' - substrateSdkVersion = '2.2.0' + substrateSdkVersion = '2.3.0' gifVersion = '1.2.19' diff --git a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/HydrationConversionFeePayment.kt b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/HydrationConversionFeePayment.kt index c5a608a0a0..4edd2629ef 100644 --- a/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/HydrationConversionFeePayment.kt +++ b/feature-account-impl/src/main/java/io/novafoundation/nova/feature_account_impl/data/fee/types/HydrationConversionFeePayment.kt @@ -27,11 +27,11 @@ internal class HydrationConversionFeePayment( ) : FeePayment { override suspend fun modifyExtrinsic(extrinsicBuilder: ExtrinsicBuilder) { - val baseCall = extrinsicBuilder.getCall() + val baseCalls = extrinsicBuilder.getCalls() extrinsicBuilder.resetCalls() extrinsicBuilder.setFeeCurrency(hydraDxAssetIdConverter.toOnChainIdOrThrow(paymentAsset)) - extrinsicBuilder.call(baseCall) + extrinsicBuilder.calls(baseCalls) extrinsicBuilder.setFeeCurrency(hydraDxAssetIdConverter.systemAssetId) } diff --git a/feature-staking-api/src/main/java/io/novafoundation/nova/feature_staking_api/domain/api/StakingRepository.kt b/feature-staking-api/src/main/java/io/novafoundation/nova/feature_staking_api/domain/api/StakingRepository.kt index ca036b6da3..04a1ad3c5f 100644 --- a/feature-staking-api/src/main/java/io/novafoundation/nova/feature_staking_api/domain/api/StakingRepository.kt +++ b/feature-staking-api/src/main/java/io/novafoundation/nova/feature_staking_api/domain/api/StakingRepository.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.feature_staking_api.domain.api import io.novafoundation.nova.feature_account_api.data.model.AccountIdMap import io.novafoundation.nova.feature_staking_api.domain.model.EraIndex import io.novafoundation.nova.feature_staking_api.domain.model.Exposure +import io.novafoundation.nova.feature_staking_api.domain.model.InflationPredictionInfo import io.novafoundation.nova.feature_staking_api.domain.model.RewardDestination import io.novafoundation.nova.feature_staking_api.domain.model.SlashingSpans import io.novafoundation.nova.feature_staking_api.domain.model.StakingLedger @@ -58,6 +59,8 @@ interface StakingRepository { suspend fun maxNominators(chainId: ChainId): BigInteger? suspend fun nominatorsCount(chainId: ChainId): BigInteger? + + suspend fun getInflationPredictionInfo(chainId: ChainId): InflationPredictionInfo } suspend fun StakingRepository.historicalEras(chainId: ChainId): List { diff --git a/feature-staking-api/src/main/java/io/novafoundation/nova/feature_staking_api/domain/model/InflationPredictionInfo.kt b/feature-staking-api/src/main/java/io/novafoundation/nova/feature_staking_api/domain/model/InflationPredictionInfo.kt new file mode 100644 index 0000000000..dd664c7442 --- /dev/null +++ b/feature-staking-api/src/main/java/io/novafoundation/nova/feature_staking_api/domain/model/InflationPredictionInfo.kt @@ -0,0 +1,46 @@ +package io.novafoundation.nova.feature_staking_api.domain.model + +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.common.utils.divideToDecimal +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import kotlin.time.Duration +import kotlin.time.Duration.Companion.days + +class InflationPredictionInfo( + val nextMint: NextMint +) { + + class NextMint( + val toStakers: Balance, + val toTreasury: Balance + ) + + companion object { + + fun fromDecoded(decoded: Any?): InflationPredictionInfo { + val asStruct = decoded.castToStruct() + + return InflationPredictionInfo( + nextMint = bindNextMint(asStruct["nextMint"]) + ) + } + + private fun bindNextMint(decoded: Any?): NextMint { + val (toStakersRaw, toTreasuryRaw) = decoded.castToList() + + return NextMint( + toStakers = bindNumber(toStakersRaw), + toTreasury = bindNumber(toTreasuryRaw) + ) + } + } +} + +fun InflationPredictionInfo.calculateStakersInflation(totalIssuance: Balance, eraDuration: Duration): Double { + val periodsInYear = 365.days / eraDuration + val inflationPerMint = nextMint.toStakers.divideToDecimal(totalIssuance) + + return inflationPerMint.toDouble() * periodsInYear +} diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/StakingRepositoryImpl.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/StakingRepositoryImpl.kt index b7d96a6ef9..a19d3af0e8 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/StakingRepositoryImpl.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/data/repository/StakingRepositoryImpl.kt @@ -16,6 +16,7 @@ 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.novafoundation.nova.feature_staking_api.domain.model.InflationPredictionInfo import io.novafoundation.nova.feature_staking_api.domain.model.Nominations import io.novafoundation.nova.feature_staking_api.domain.model.SlashingSpans import io.novafoundation.nova.feature_staking_api.domain.model.StakingLedger @@ -43,6 +44,7 @@ import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.update import io.novafoundation.nova.feature_staking_impl.data.network.blockhain.updaters.activeEraStorageKey import io.novafoundation.nova.feature_staking_impl.data.repository.datasource.StakingStoriesDataSource import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletConstants +import io.novafoundation.nova.runtime.call.MultiChainRuntimeCallsApi 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 @@ -75,6 +77,7 @@ class StakingRepositoryImpl( private val chainRegistry: ChainRegistry, private val stakingStoriesDataSource: StakingStoriesDataSource, private val storageCache: StorageCache, + private val multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi, ) : StakingRepository { override suspend fun eraStartSessionIndex(chainId: ChainId, currentEra: BigInteger): EraIndex { @@ -266,6 +269,17 @@ class StakingRepositoryImpl( chainId = chainId ) + override suspend fun getInflationPredictionInfo(chainId: ChainId): InflationPredictionInfo { + val callApi = multiChainRuntimeCallsApi.forChain(chainId) + + return callApi.call( + section = "Inflation", + method = "experimental_inflation_prediction_info", + arguments = emptyMap(), + returnBinding = InflationPredictionInfo::fromDecoded + ) + } + private suspend fun queryStorageIfExists( chainId: ChainId, storageName: String, @@ -318,6 +332,7 @@ class StakingRepositoryImpl( stashId, prefs ) + nominations != null -> StakingState.Stash.Nominator( chain, chainAsset, diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/StakingFeatureModule.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/StakingFeatureModule.kt index 492950cbc0..c34e31b46a 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/StakingFeatureModule.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/di/StakingFeatureModule.kt @@ -70,9 +70,9 @@ import io.novafoundation.nova.feature_staking_impl.data.repository.datasource.re import io.novafoundation.nova.feature_staking_impl.data.repository.datasource.reward.PoolStakingRewardsDataSource import io.novafoundation.nova.feature_staking_impl.data.repository.datasource.reward.RealStakingRewardsDataSourceRegistry import io.novafoundation.nova.feature_staking_impl.data.repository.datasource.reward.StakingRewardsDataSourceRegistry -import io.novafoundation.nova.feature_staking_impl.data.validators.ValidatorsPreferencesSource import io.novafoundation.nova.feature_staking_impl.data.validators.NovaValidatorsApi import io.novafoundation.nova.feature_staking_impl.data.validators.RemoteValidatorsPreferencesSource +import io.novafoundation.nova.feature_staking_impl.data.validators.ValidatorsPreferencesSource 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.domain.StakingInteractor @@ -177,7 +177,8 @@ class StakingFeatureModule { stakingStoriesDataSource: StakingStoriesDataSource, walletConstants: WalletConstants, chainRegistry: ChainRegistry, - storageCache: StorageCache + storageCache: StorageCache, + multiChainRuntimeCallsApi: MultiChainRuntimeCallsApi ): StakingRepository = StakingRepositoryImpl( accountStakingDao = accountStakingDao, remoteStorage = remoteStorageSource, @@ -185,7 +186,8 @@ class StakingFeatureModule { stakingStoriesDataSource = stakingStoriesDataSource, walletConstants = walletConstants, chainRegistry = chainRegistry, - storageCache = storageCache + storageCache = storageCache, + multiChainRuntimeCallsApi = multiChainRuntimeCallsApi ) @Provides diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/rewards/InflationPredictionInfoCalculator.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/rewards/InflationPredictionInfoCalculator.kt new file mode 100644 index 0000000000..d8ed7fb6f5 --- /dev/null +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/rewards/InflationPredictionInfoCalculator.kt @@ -0,0 +1,18 @@ +package io.novafoundation.nova.feature_staking_impl.domain.rewards + +import io.novafoundation.nova.feature_staking_api.domain.model.InflationPredictionInfo +import io.novafoundation.nova.feature_staking_api.domain.model.calculateStakersInflation +import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance +import kotlin.time.Duration + +class InflationPredictionInfoCalculator( + private val inflationPredictionInfo: InflationPredictionInfo, + private val eraDuration: Duration, + private val totalIssuance: Balance, + validators: List +) : InflationBasedRewardCalculator(validators, totalIssuance) { + + override fun calculateYearlyInflation(stakedPortion: Double): Double { + return inflationPredictionInfo.calculateStakersInflation(totalIssuance, eraDuration) + } +} diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/rewards/RewardCalculatorFactory.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/rewards/RewardCalculatorFactory.kt index e4cbeb12bf..c28f959bb0 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/rewards/RewardCalculatorFactory.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/rewards/RewardCalculatorFactory.kt @@ -14,6 +14,7 @@ import io.novafoundation.nova.feature_staking_impl.data.stakingType import io.novafoundation.nova.feature_staking_impl.data.unwrapNominationPools import io.novafoundation.nova.feature_staking_impl.domain.common.StakingSharedComputation import io.novafoundation.nova.feature_staking_impl.domain.common.electedExposuresInActiveEra +import io.novafoundation.nova.feature_staking_impl.domain.common.eraTimeCalculator import io.novafoundation.nova.feature_staking_impl.domain.error.accountIdNotFound import io.novafoundation.nova.runtime.ext.Geneses import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -42,7 +43,8 @@ class RewardCalculatorFactory( suspend fun create( stakingOption: StakingOption, exposures: AccountIdMap, - validatorsPrefs: AccountIdMap + validatorsPrefs: AccountIdMap, + scope: CoroutineScope ): RewardCalculator = withContext(Dispatchers.Default) { val totalIssuance = totalIssuanceRepository.getTotalIssuance(stakingOption.assetWithChain.chain.id) @@ -57,7 +59,7 @@ class RewardCalculatorFactory( ) } - stakingOption.createRewardCalculator(validators, totalIssuance) + stakingOption.createRewardCalculator(validators, totalIssuance, scope) } suspend fun create(stakingOption: StakingOption, scope: CoroutineScope): RewardCalculator = withContext(Dispatchers.Default) { @@ -66,13 +68,17 @@ class RewardCalculatorFactory( val exposures = shareStakingSharedComputation.get().electedExposuresInActiveEra(chainId, scope) val validatorsPrefs = stakingRepository.getValidatorPrefs(chainId, exposures.keys) - create(stakingOption, exposures, validatorsPrefs) + create(stakingOption, exposures, validatorsPrefs, scope) } - private suspend fun StakingOption.createRewardCalculator(validators: List, totalIssuance: BigInteger): RewardCalculator { + private suspend fun StakingOption.createRewardCalculator( + validators: List, + totalIssuance: BigInteger, + scope: CoroutineScope + ): RewardCalculator { return when (unwrapNominationPools().stakingType) { RELAYCHAIN, RELAYCHAIN_AURA -> { - val custom = customRelayChainCalculator(validators, totalIssuance) + val custom = customRelayChainCalculator(validators, totalIssuance, scope) if (custom != null) return custom val activePublicParachains = parasRepository.activePublicParachains(assetWithChain.chain.id) @@ -88,10 +94,12 @@ class RewardCalculatorFactory( private suspend fun StakingOption.customRelayChainCalculator( validators: List, - totalIssuance: BigInteger + totalIssuance: BigInteger, + scope: CoroutineScope ): RewardCalculator? { return when (chain.id) { Chain.Geneses.VARA -> Vara(chain.id, validators, totalIssuance) + Chain.Geneses.POLKADOT -> PolkadotInflationPrediction(validators, totalIssuance, scope) else -> null } } @@ -119,4 +127,28 @@ class RewardCalculatorFactory( } .getOrNull() } + + private suspend fun StakingOption.PolkadotInflationPrediction( + validators: List, + totalIssuance: BigInteger, + scope: CoroutineScope + ): RewardCalculator? { + return runCatching { + val eraRewardCalculator = shareStakingSharedComputation.get().eraTimeCalculator(this, scope) + val eraDuration = eraRewardCalculator.eraDuration() + + val inflationPredictionInfo = stakingRepository.getInflationPredictionInfo(chain.id) + + InflationPredictionInfoCalculator( + inflationPredictionInfo = inflationPredictionInfo, + eraDuration = eraDuration, + totalIssuance = totalIssuance, + validators = validators + ) + } + .onFailure { + Log.e("RewardCalculatorFactory", "Failed to create Polkadot Inflation Prediction reward calculator, fallbacking to default", it) + } + .getOrNull() + } } diff --git a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validators/ValidatorProvider.kt b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validators/ValidatorProvider.kt index 2c01a3d0ba..b6f6f95c7e 100644 --- a/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validators/ValidatorProvider.kt +++ b/feature-staking-impl/src/main/java/io/novafoundation/nova/feature_staking_impl/domain/validators/ValidatorProvider.kt @@ -55,7 +55,7 @@ class ValidatorProvider( val identities = identityRepository.getIdentitiesFromIdsHex(chainId, requestedValidatorIds) val slashes = stakingRepository.getSlashes(chainId, requestedValidatorIds) - val rewardCalculator = rewardCalculatorFactory.create(stakingOption, electedValidatorExposures, validatorPrefs) + val rewardCalculator = rewardCalculatorFactory.create(stakingOption, electedValidatorExposures, validatorPrefs, scope) val maxNominators = stakingConstantsRepository.maxRewardedNominatorPerValidator(chainId) return requestedValidatorIds.map { accountIdHex -> diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/call/RuntimeCallsApi.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/call/RuntimeCallsApi.kt index b6113a54db..b69f25dc29 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/call/RuntimeCallsApi.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/call/RuntimeCallsApi.kt @@ -1,7 +1,6 @@ package io.novafoundation.nova.runtime.call import io.novafoundation.nova.common.data.network.runtime.binding.fromHexOrIncompatible -import io.novafoundation.nova.runtime.network.rpc.StateCallRequest import io.novafoundation.nova.runtime.network.rpc.stateCall import io.novasama.substrate_sdk_android.extensions.requireHexPrefix import io.novasama.substrate_sdk_android.extensions.toHexString @@ -9,7 +8,12 @@ import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot import io.novasama.substrate_sdk_android.runtime.definitions.registry.TypeRegistry import io.novasama.substrate_sdk_android.runtime.definitions.registry.getOrThrow import io.novasama.substrate_sdk_android.runtime.definitions.types.bytes +import io.novasama.substrate_sdk_android.runtime.metadata.createRequest +import io.novasama.substrate_sdk_android.runtime.metadata.decodeOutput +import io.novasama.substrate_sdk_android.runtime.metadata.method +import io.novasama.substrate_sdk_android.runtime.metadata.runtimeApi import io.novasama.substrate_sdk_android.wsrpc.SocketService +import io.novasama.substrate_sdk_android.wsrpc.request.runtime.state.StateCallRequest typealias RuntimeTypeName = String typealias RuntimeTypeValue = Any? @@ -18,13 +22,13 @@ interface RuntimeCallsApi { val runtime: RuntimeSnapshot - // TODO we can do better than that - it is possible to auto-detect method signature's types - // However it requires a separate research - // We should revisit this when Metadata v15 will take place /** * @param arguments - list of pairs [runtimeTypeValue, runtimeTypeName], * where runtimeTypeValue is value to be encoded and runtimeTypeName is type name that can be found in [TypeRegistry] * It can also be null, in that case argument is considered as already encoded in hex form + * + * This should only be used if automatic decoding via metadata is not possible + * For the other cases use another [call] overload */ suspend fun call( section: String, @@ -33,6 +37,13 @@ interface RuntimeCallsApi { returnType: RuntimeTypeName, returnBinding: (Any?) -> R ): R + + suspend fun call( + section: String, + method: String, + arguments: Map, + returnBinding: (Any?) -> R + ): R } internal class RealRuntimeCallsApi( @@ -58,6 +69,22 @@ internal class RealRuntimeCallsApi( return returnBinding(decoded) } + override suspend fun call( + section: String, + method: String, + arguments: Map, + returnBinding: (Any?) -> R + ): R { + val apiMethod = runtime.metadata.runtimeApi(section).method(method) + val request = apiMethod.createRequest(runtime, arguments) + + val response = socketService.stateCall(request) + + val decoded = response?.let { apiMethod.decodeOutput(runtime, it) } + + return returnBinding(decoded) + } + private fun decodeResponse(responseHex: String?, returnTypeName: String): Any? { val returnType = runtime.typeRegistry.getOrThrow(returnTypeName) diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/RuntimeMetadataFetcher.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/RuntimeMetadataFetcher.kt index 9ceade987c..04236ee624 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/RuntimeMetadataFetcher.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/runtime/RuntimeMetadataFetcher.kt @@ -2,7 +2,6 @@ package io.novafoundation.nova.runtime.multiNetwork.runtime import android.util.Log import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId -import io.novafoundation.nova.runtime.network.rpc.StateCallRequest import io.novafoundation.nova.runtime.network.rpc.stateCall import io.novasama.substrate_sdk_android.extensions.fromHex import io.novasama.substrate_sdk_android.runtime.metadata.GetMetadataRequest @@ -13,6 +12,7 @@ import io.novasama.substrate_sdk_android.wsrpc.SocketService import io.novasama.substrate_sdk_android.wsrpc.executeAsync import io.novasama.substrate_sdk_android.wsrpc.mappers.nonNull import io.novasama.substrate_sdk_android.wsrpc.mappers.pojo +import io.novasama.substrate_sdk_android.wsrpc.request.runtime.state.StateCallRequest private const val LATEST_SUPPORTED_METADATA_VERSION = 15 diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/network/rpc/RpcCalls.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/network/rpc/RpcCalls.kt index 905aab2f1a..15c6577bf3 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/network/rpc/RpcCalls.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/network/rpc/RpcCalls.kt @@ -40,6 +40,7 @@ import io.novasama.substrate_sdk_android.wsrpc.request.runtime.author.SubmitAndW import io.novasama.substrate_sdk_android.wsrpc.request.runtime.author.SubmitExtrinsicRequest import io.novasama.substrate_sdk_android.wsrpc.request.runtime.chain.RuntimeVersionFull import io.novasama.substrate_sdk_android.wsrpc.request.runtime.chain.RuntimeVersionRequest +import io.novasama.substrate_sdk_android.wsrpc.request.runtime.state.StateCallRequest import io.novasama.substrate_sdk_android.wsrpc.subscriptionFlow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emitAll diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/network/rpc/StateCallRequest.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/network/rpc/StateCallRequest.kt deleted file mode 100644 index e154fd1c47..0000000000 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/network/rpc/StateCallRequest.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.novafoundation.nova.runtime.network.rpc - -import io.novasama.substrate_sdk_android.wsrpc.request.runtime.RuntimeRequest - -class StateCallRequest( - runtimeRpcName: String, - vararg params: Any -) : RuntimeRequest( - "state_call", - listOf(runtimeRpcName) + params -)