Initial commit: Pezkuwi Wallet Android

Security hardened release:
- Code obfuscation enabled (minifyEnabled=true, shrinkResources=true)
- Sensitive files excluded (google-services.json, keystores)
- Branch.io key moved to BuildConfig placeholder
- Updated dependencies: OkHttp 4.12.0, Gson 2.10.1, BouncyCastle 1.77
- Comprehensive ProGuard rules for crypto wallet
- Navigation 2.7.7, Lifecycle 2.7.0, ConstraintLayout 2.1.4
This commit is contained in:
2026-02-12 05:19:41 +03:00
commit a294aa1a6b
7687 changed files with 441811 additions and 0 deletions
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
</manifest>
@@ -0,0 +1,16 @@
package io.novafoundation.nova.feature_staking_api.data.dashboard
import io.novafoundation.nova.feature_staking_api.domain.dashboard.model.AggregatedStakingDashboardOption.SyncingStage
import io.novafoundation.nova.feature_staking_api.domain.dashboard.model.StakingOptionId
import kotlinx.coroutines.flow.Flow
interface StakingDashboardSyncTracker {
val syncedItemsFlow: Flow<SyncingStageMap>
}
typealias SyncingStageMap = Map<StakingOptionId, SyncingStage>
fun SyncingStageMap.getSyncingStage(stakingOptionId: StakingOptionId): SyncingStage {
return get(stakingOptionId) ?: SyncingStage.SYNCING_ALL
}
@@ -0,0 +1,5 @@
package io.novafoundation.nova.feature_staking_api.data.dashboard
import io.novafoundation.nova.core.updater.UpdateSystem
interface StakingDashboardUpdateSystem : UpdateSystem, StakingDashboardSyncTracker
@@ -0,0 +1,22 @@
package io.novafoundation.nova.feature_staking_api.data.dashboard.common
import io.novafoundation.nova.runtime.ext.isEnabled
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.ChainsById
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.findChains
import io.novafoundation.nova.runtime.multiNetwork.findChainsById
suspend fun ChainRegistry.stakingChains(): List<Chain> {
return findChains { it.isEnabled && it.supportedStakingOptions() }
}
suspend fun ChainRegistry.stakingChainsById(): ChainsById {
return findChainsById { it.isEnabled && it.supportedStakingOptions() }
}
fun Chain.supportedStakingOptions(): Boolean {
return utilityAsset.supportedStakingOptions().isNotEmpty()
}
@@ -0,0 +1,9 @@
package io.novafoundation.nova.feature_staking_api.data.mythos
import io.novafoundation.nova.feature_account_api.domain.account.system.SystemAccountMatcher
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
interface MythosMainPotMatcherFactory {
suspend fun create(chainAsset: Chain.Asset): SystemAccountMatcher?
}
@@ -0,0 +1,10 @@
package io.novafoundation.nova.feature_staking_api.data.network.blockhain.updaters
import io.novafoundation.nova.core.updater.Updater
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
interface PooledBalanceUpdaterFactory {
fun create(chain: Chain): Updater<MetaAccount>
}
@@ -0,0 +1,36 @@
package io.novafoundation.nova.feature_staking_api.data.nominationPools.pool
import io.novafoundation.nova.common.address.AccountIdKey
import io.novafoundation.nova.feature_account_api.domain.account.system.SystemAccountMatcher
import io.novafoundation.nova.feature_staking_api.domain.nominationPool.model.PoolId
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novasama.substrate_sdk_android.runtime.AccountId
interface PoolAccountDerivation {
enum class PoolAccountType {
BONDED, REWARD
}
suspend fun derivePoolAccount(poolId: PoolId, derivationType: PoolAccountType, chainId: ChainId): AccountId
/**
* Derives pool accounts with poolId from range 1..[numberOfPools] (end-inclusive)
*/
suspend fun derivePoolAccountsRange(numberOfPools: Int, derivationType: PoolAccountType, chainId: ChainId): Map<PoolId, AccountIdKey>
suspend fun poolAccountMatcher(derivationType: PoolAccountType, chainId: ChainId): SystemAccountMatcher?
}
suspend fun PoolAccountDerivation.poolRewardAccountMatcher(chainId: ChainId): SystemAccountMatcher? {
return poolAccountMatcher(PoolAccountDerivation.PoolAccountType.REWARD, chainId)
}
suspend fun PoolAccountDerivation.bondedAccountOf(poolId: PoolId, chainId: ChainId): AccountId {
return derivePoolAccount(poolId, PoolAccountDerivation.PoolAccountType.BONDED, chainId)
}
suspend fun PoolAccountDerivation.deriveAllBondedPools(lastPoolId: PoolId, chainId: ChainId): Map<PoolId, AccountIdKey> {
return derivePoolAccountsRange(numberOfPools = lastPoolId.value.toInt(), PoolAccountDerivation.PoolAccountType.BONDED, chainId)
}
@@ -0,0 +1,20 @@
package io.novafoundation.nova.feature_staking_api.data.parachainStaking.turing.repository
import java.math.BigInteger
class OptimalAutomationRequest(
val collator: String,
val amount: BigInteger,
)
data class OptimalAutomationResponse(
val apy: Double,
val period: Int,
)
enum class AutomationAction(val rpcParamName: String) {
NOTIFY("Notify"),
NATIVE_TRANSFER("NativeTransfer"),
XCMP("XCMP"),
AUTO_COMPOUND_DELEGATED_STAKE("AutoCompoundDelegatedStake"),
}
@@ -0,0 +1,20 @@
package io.novafoundation.nova.feature_staking_api.data.parachainStaking.turing.repository
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
import java.math.BigInteger
import io.novasama.substrate_sdk_android.runtime.AccountId
class TuringAutomationTask(
val id: String,
val delegator: AccountId,
val collator: AccountId,
val accountMinimum: Balance,
val schedule: Schedule
) {
sealed interface Schedule {
object Unknown : Schedule
class Recurring(val frequency: BigInteger) : Schedule
}
}
@@ -0,0 +1,15 @@
package io.novafoundation.nova.feature_staking_api.data.parachainStaking.turing.repository
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novasama.substrate_sdk_android.runtime.AccountId
import kotlinx.coroutines.flow.Flow
interface TuringAutomationTasksRepository {
fun automationTasksFlow(chainId: ChainId, accountId: AccountId): Flow<List<TuringAutomationTask>>
suspend fun calculateOptimalAutomation(chainId: ChainId, request: OptimalAutomationRequest): OptimalAutomationResponse
suspend fun getTimeAutomationFees(chainId: ChainId, action: AutomationAction, executions: Int): Balance
}
@@ -0,0 +1,7 @@
package io.novafoundation.nova.feature_staking_api.di
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Staking
@@ -0,0 +1,32 @@
package io.novafoundation.nova.feature_staking_api.di
import io.novafoundation.nova.feature_staking_api.data.dashboard.StakingDashboardUpdateSystem
import io.novafoundation.nova.feature_staking_api.data.mythos.MythosMainPotMatcherFactory
import io.novafoundation.nova.feature_staking_api.data.network.blockhain.updaters.PooledBalanceUpdaterFactory
import io.novafoundation.nova.feature_staking_api.data.nominationPools.pool.PoolAccountDerivation
import io.novafoundation.nova.feature_staking_api.data.parachainStaking.turing.repository.TuringAutomationTasksRepository
import io.novafoundation.nova.feature_staking_api.di.deeplinks.StakingDeepLinks
import io.novafoundation.nova.feature_staking_api.domain.api.StakingRepository
import io.novafoundation.nova.feature_staking_api.domain.dashboard.StakingDashboardInteractor
import io.novafoundation.nova.feature_staking_api.presentation.nominationPools.display.PoolDisplayUseCase
interface StakingFeatureApi {
fun repository(): StakingRepository
val turingAutomationRepository: TuringAutomationTasksRepository
val dashboardInteractor: StakingDashboardInteractor
val dashboardUpdateSystem: StakingDashboardUpdateSystem
val pooledBalanceUpdaterFactory: PooledBalanceUpdaterFactory
val poolDisplayUseCase: PoolDisplayUseCase
val poolAccountDerivation: PoolAccountDerivation
val mythosMainPotMatcherFactory: MythosMainPotMatcherFactory
val stakingDeepLinks: StakingDeepLinks
}
@@ -0,0 +1,5 @@
package io.novafoundation.nova.feature_staking_api.di.deeplinks
import io.novafoundation.nova.feature_deep_linking.presentation.handling.DeepLinkHandler
class StakingDeepLinks(val deepLinkHandlers: List<DeepLinkHandler>)
@@ -0,0 +1,73 @@
package io.novafoundation.nova.feature_staking_api.domain.api
import io.novafoundation.nova.common.address.AccountIdKey
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
import io.novafoundation.nova.feature_staking_api.domain.model.ValidatorPrefs
import io.novafoundation.nova.feature_staking_api.domain.model.relaychain.StakingState
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novasama.substrate_sdk_android.runtime.AccountId
import kotlinx.coroutines.flow.Flow
import java.math.BigInteger
typealias ExposuresWithEraIndex = Pair<AccountIdMap<Exposure>, EraIndex>
interface StakingRepository {
suspend fun eraStartSessionIndex(chainId: ChainId, era: EraIndex): EraIndex
suspend fun eraLength(chain: Chain): BigInteger
suspend fun getActiveEraIndex(chainId: ChainId): EraIndex
suspend fun getCurrentEraIndex(chainId: ChainId): EraIndex
suspend fun getHistoryDepth(chainId: ChainId): BigInteger
fun observeActiveEraIndex(chainId: ChainId): Flow<EraIndex>
suspend fun getElectedValidatorsExposure(chainId: ChainId, eraIndex: EraIndex): AccountIdMap<Exposure>
suspend fun getValidatorPrefs(chainId: ChainId, accountIdsHex: Collection<String>): AccountIdMap<ValidatorPrefs?>
suspend fun getSlashes(chainId: ChainId, accountIdsHex: Collection<String>): Set<AccountIdKey>
suspend fun getSlashingSpan(chainId: ChainId, accountId: AccountId): SlashingSpans?
fun stakingStateFlow(
chain: Chain,
chainAsset: Chain.Asset,
accountId: AccountId
): Flow<StakingState>
fun ledgerFlow(stakingState: StakingState.Stash): Flow<StakingLedger>
suspend fun ledger(chainId: ChainId, accountId: AccountId): StakingLedger?
suspend fun getRewardDestination(stakingState: StakingState.Stash): RewardDestination
suspend fun minimumNominatorBond(chainId: ChainId): BigInteger
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<BigInteger> {
val activeEra = getActiveEraIndex(chainId).toInt()
val currentEra = getCurrentEraIndex(chainId).toInt()
val historyDepth = getHistoryDepth(chainId).toInt()
val startingIndex = (currentEra - historyDepth).coerceAtLeast(0)
val historicalRange = startingIndex until activeEra
return historicalRange.map(Int::toBigInteger)
}
@@ -0,0 +1,15 @@
package io.novafoundation.nova.feature_staking_api.domain.dashboard
import io.novafoundation.nova.common.domain.ExtendedLoadingState
import io.novafoundation.nova.feature_staking_api.domain.dashboard.model.MoreStakingOptions
import io.novafoundation.nova.feature_staking_api.domain.dashboard.model.StakingDashboard
import kotlinx.coroutines.flow.Flow
interface StakingDashboardInteractor {
suspend fun syncDapps()
fun stakingDashboardFlow(): Flow<ExtendedLoadingState<StakingDashboard>>
fun moreStakingOptionsFlow(): Flow<MoreStakingOptions>
}
@@ -0,0 +1,69 @@
package io.novafoundation.nova.feature_staking_api.domain.dashboard.model
import io.novafoundation.nova.common.domain.ExtendedLoadingState
import io.novafoundation.nova.common.utils.Percent
import io.novafoundation.nova.feature_staking_api.domain.dashboard.model.AggregatedStakingDashboardOption.NoStake.FlowType
import io.novafoundation.nova.feature_staking_api.domain.dashboard.model.AggregatedStakingDashboardOption.SyncingStage
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
import io.novafoundation.nova.feature_wallet_api.domain.model.Token
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
class AggregatedStakingDashboardOption<out S>(
val chain: Chain,
val token: Token,
val stakingState: S,
val syncingStage: SyncingStage
) {
class HasStake(
val showStakingType: Boolean,
val stakingType: Chain.Asset.StakingType,
val stake: Balance,
val stats: ExtendedLoadingState<Stats>,
) {
class Stats(val rewards: Balance, val estimatedEarnings: Percent, val status: StakingStatus)
enum class StakingStatus {
ACTIVE, INACTIVE, WAITING
}
}
sealed interface WithoutStake
class NoStake(val stats: ExtendedLoadingState<Stats>, val flowType: FlowType, val availableBalance: Balance) : WithoutStake {
sealed class FlowType {
class Aggregated(val stakingTypes: List<Chain.Asset.StakingType>) : FlowType()
class Single(val stakingType: Chain.Asset.StakingType, val showStakingType: Boolean) : FlowType()
}
class Stats(val estimatedEarnings: Percent)
}
object NotYetResolved : WithoutStake
enum class SyncingStage {
SYNCING_ALL, SYNCING_SECONDARY, SYNCED
}
}
val FlowType.allStakingTypes: List<Chain.Asset.StakingType>
get() = when (this) {
is FlowType.Aggregated -> stakingTypes
is FlowType.Single -> listOf(stakingType)
}
fun SyncingStage.isSyncing(): Boolean {
return this != SyncingStage.SYNCED
}
fun SyncingStage.isSyncingPrimary(): Boolean {
return this == SyncingStage.SYNCING_ALL
}
fun SyncingStage.isSyncingSecondary(): Boolean {
return this < SyncingStage.SYNCED
}
@@ -0,0 +1,21 @@
package io.novafoundation.nova.feature_staking_api.domain.dashboard.model
import io.novafoundation.nova.common.domain.ExtendedLoadingState
import io.novafoundation.nova.feature_staking_api.domain.dashboard.model.AggregatedStakingDashboardOption.HasStake
import io.novafoundation.nova.feature_staking_api.domain.dashboard.model.AggregatedStakingDashboardOption.WithoutStake
class StakingDashboard(
val hasStake: List<AggregatedStakingDashboardOption<HasStake>>,
val withoutStake: List<AggregatedStakingDashboardOption<WithoutStake>>,
)
class MoreStakingOptions(
val inAppStaking: List<AggregatedStakingDashboardOption<WithoutStake>>,
val browserStaking: ExtendedLoadingState<List<StakingDApp>>,
)
class StakingDApp(
val url: String,
val iconUrl: String,
val name: String
)
@@ -0,0 +1,9 @@
package io.novafoundation.nova.feature_staking_api.domain.dashboard.model
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainAssetId
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
data class StakingOptionId(val chainId: ChainId, val chainAssetId: ChainAssetId, val stakingType: Chain.Asset.StakingType)
data class MultiStakingOptionIds(val chainId: ChainId, val chainAssetId: ChainAssetId, val stakingTypes: List<Chain.Asset.StakingType>)
@@ -0,0 +1,16 @@
package io.novafoundation.nova.feature_staking_api.domain.model
@JvmInline
value class BondedEras(val value: List<BondedEra>) {
companion object
}
class BondedEra(val era: EraIndex, val startSessionIndex: SessionIndex) {
companion object
}
fun BondedEras.findStartSessionIndexOf(era: EraIndex): SessionIndex? {
return value.find { it.era == era }?.startSessionIndex
}
@@ -0,0 +1,5 @@
package io.novafoundation.nova.feature_staking_api.domain.model
import java.math.BigInteger
typealias EraIndex = BigInteger
@@ -0,0 +1,25 @@
package io.novafoundation.nova.feature_staking_api.domain.model
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
import java.math.BigInteger
interface EraRedeemable {
val redeemEra: EraIndex
companion object
}
interface RedeemableAmount : EraRedeemable {
val amount: Balance
}
fun EraRedeemable.isUnbondingIn(activeEraIndex: BigInteger) = redeemEra > activeEraIndex
fun EraRedeemable.isRedeemableIn(activeEraIndex: BigInteger) = redeemEra <= activeEraIndex
fun EraRedeemable.Companion.of(eraIndex: EraIndex): EraRedeemable = InlineEraRedeemable(eraIndex)
@JvmInline
private value class InlineEraRedeemable(override val redeemEra: EraIndex) : EraRedeemable
@@ -0,0 +1,16 @@
package io.novafoundation.nova.feature_staking_api.domain.model
import java.math.BigInteger
class IndividualExposure(val who: ByteArray, val value: BigInteger)
class Exposure(val total: BigInteger, val own: BigInteger, val others: List<IndividualExposure>)
class ExposureOverview(val total: BigInteger, val own: BigInteger, val pageCount: BigInteger, val nominatorCount: BigInteger)
class ExposurePage(val others: List<IndividualExposure>)
class PagedExposure(
val overview: ExposureOverview,
val pages: List<ExposurePage>
)
@@ -0,0 +1,47 @@
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.math.roundToInt
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).roundToInt()
val inflationPerMint = nextMint.toStakers.divideToDecimal(totalIssuance)
return inflationPerMint.toDouble() * periodsInYear
}
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_staking_api.domain.model
import java.math.BigInteger
class NominatedValidator(
val validator: Validator,
val status: Status,
) {
sealed class Status {
class Active(val nomination: BigInteger, val willUserBeRewarded: Boolean) : Status()
object Elected : Status()
object Inactive : Status()
object WaitingForNextEra : Status()
sealed class Group(val numberOfValidators: Int, val position: Int) {
companion object {
val COMPARATOR = Comparator.comparingInt<Group> { it.position }
}
class Active(numberOfValidators: Int) : Group(numberOfValidators, 0)
class Elected(numberOfValidators: Int) : Group(numberOfValidators, 1)
class Inactive(numberOfValidators: Int) : Group(numberOfValidators, 2)
class WaitingForNextEra(val maxValidatorsPerNominator: Int, numberOfValidators: Int) : Group(numberOfValidators, 3)
}
}
}
@@ -0,0 +1,9 @@
package io.novafoundation.nova.feature_staking_api.domain.model
import io.novasama.substrate_sdk_android.runtime.AccountId
class Nominations(
val targets: List<AccountId>,
val submittedInEra: EraIndex,
val suppressed: Boolean
)
@@ -0,0 +1,10 @@
package io.novafoundation.nova.feature_staking_api.domain.model
import io.novasama.substrate_sdk_android.runtime.AccountId
sealed class RewardDestination {
object Restake : RewardDestination()
class Payout(val targetAccountId: AccountId) : RewardDestination()
}
@@ -0,0 +1,5 @@
package io.novafoundation.nova.feature_staking_api.domain.model
import java.math.BigInteger
typealias SessionIndex = BigInteger
@@ -0,0 +1,17 @@
package io.novafoundation.nova.feature_staking_api.domain.model
import java.math.BigInteger
class SlashingSpans(
val lastNonZeroSlash: EraIndex,
val prior: List<EraIndex>
)
fun SlashingSpans?.numberOfSlashingSpans(): BigInteger {
if (this == null) return BigInteger.ZERO
// all from prior + one for lastNonZeroSlash
val numberOfSlashingSpans = prior.size + 1
return numberOfSlashingSpans.toBigInteger()
}
@@ -0,0 +1,6 @@
package io.novafoundation.nova.feature_staking_api.domain.model
class StakingAccount(
val address: String,
val name: String?,
)
@@ -0,0 +1,32 @@
package io.novafoundation.nova.feature_staking_api.domain.model
import io.novafoundation.nova.common.utils.orZero
import io.novafoundation.nova.common.utils.sumByBigInteger
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
import io.novasama.substrate_sdk_android.runtime.AccountId
import java.math.BigInteger
class StakingLedger(
val stashId: AccountId,
val total: BigInteger,
val active: BigInteger,
val unlocking: List<UnlockChunk>,
val claimedRewards: List<BigInteger>
)
class UnlockChunk(override val amount: BigInteger, val era: BigInteger) : RedeemableAmount {
override val redeemEra: EraIndex = era
}
fun List<UnlockChunk>.totalRedeemableIn(activeEra: EraIndex): Balance = sumStaking { it.isRedeemableIn(activeEra) }
fun List<UnlockChunk>.sumStaking(
condition: (chunk: UnlockChunk) -> Boolean
): BigInteger {
return filter { condition(it) }
.sumByBigInteger(UnlockChunk::amount)
}
fun StakingLedger?.activeBalance(): Balance {
return this?.active.orZero()
}
@@ -0,0 +1,31 @@
package io.novafoundation.nova.feature_staking_api.domain.model
import io.novafoundation.nova.common.utils.Identifiable
import io.novafoundation.nova.feature_account_api.data.model.OnChainIdentity
import java.math.BigDecimal
import java.math.BigInteger
typealias Commission = BigDecimal
class ValidatorPrefs(val commission: Commission, val blocked: Boolean)
class Validator(
val address: String,
val slashed: Boolean,
val accountIdHex: String,
val prefs: ValidatorPrefs?,
val electedInfo: ElectedInfo?,
val identity: OnChainIdentity?,
val isNovaValidator: Boolean
) : Identifiable {
class ElectedInfo(
val totalStake: BigInteger,
val ownStake: BigInteger,
val nominatorStakes: List<IndividualExposure>,
val apy: BigDecimal,
val isOversubscribed: Boolean
)
override val identifier: String = address
}
@@ -0,0 +1,89 @@
package io.novafoundation.nova.feature_staking_api.domain.model.parachain
import io.novafoundation.nova.common.data.network.runtime.binding.BalanceOf
import io.novafoundation.nova.common.utils.castOrNull
import io.novafoundation.nova.common.utils.orZero
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks
import io.novafoundation.nova.runtime.ext.addressOf
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.extensions.toHexString
import io.novasama.substrate_sdk_android.runtime.AccountId
import java.math.BigDecimal
import java.math.BigInteger
sealed class DelegatorState(
val chain: Chain,
val chainAsset: Chain.Asset,
) {
class Delegator(
val accountId: AccountId,
chain: Chain,
chainAsset: Chain.Asset,
val delegations: List<DelegatorBond>,
val total: Balance,
val lessTotal: Balance,
) : DelegatorState(chain, chainAsset)
class None(chain: Chain, chainAsset: Chain.Asset) : DelegatorState(chain, chainAsset)
}
fun DelegatorState.stakeablePlanks(freeBalance: BigInteger): BigInteger {
return (freeBalance - totalBonded).coerceAtLeast(BigInteger.ZERO)
}
fun DelegatorState.stakeableAmount(freeBalance: BigInteger): BigDecimal {
return chainAsset.amountFromPlanks(stakeablePlanks(freeBalance))
}
private val DelegatorState.totalBonded: BigInteger
get() = asDelegator()?.total.orZero()
val DelegatorState.activeBonded: Balance
get() = when (this) {
is DelegatorState.Delegator -> total - lessTotal
is DelegatorState.None -> Balance.ZERO
}
val DelegatorState.delegationsCount
get() = when (this) {
is DelegatorState.Delegator -> delegations.size
is DelegatorState.None -> 0
}
fun DelegatorState.Delegator.delegatedCollatorIds() = delegations.map { it.owner }
fun DelegatorState.Delegator.delegatedCollatorIdsHex() = delegations.map { it.owner.toHexString() }
fun DelegatorState.delegationAmountTo(collatorId: AccountId): BalanceOf? {
return castOrNull<DelegatorState.Delegator>()?.delegations?.find { it.owner.contentEquals(collatorId) }?.balance
}
fun DelegatorState.hasDelegation(collatorId: AccountId): Boolean = this is DelegatorState.Delegator && delegations.any { it.owner.contentEquals(collatorId) }
fun DelegatorState.asDelegator(): DelegatorState.Delegator? = castOrNull<DelegatorState.Delegator>()
class DelegatorBond(
val owner: AccountId,
val balance: BalanceOf,
)
class ScheduledDelegationRequest(
val collator: AccountId,
val delegator: AccountId,
val whenExecutable: RoundIndex,
val action: DelegationAction
)
fun ScheduledDelegationRequest.redeemableIn(roundIndex: RoundIndex): Boolean = whenExecutable <= roundIndex
fun ScheduledDelegationRequest.unbondingIn(roundIndex: RoundIndex): Boolean = whenExecutable > roundIndex
sealed class DelegationAction(val amount: BalanceOf) {
class Revoke(amount: Balance) : DelegationAction(amount)
class Decrease(amount: Balance) : DelegationAction(amount)
}
typealias RoundIndex = BigInteger
fun DelegatorState.Delegator.address() = chain.addressOf(accountId)
@@ -0,0 +1,62 @@
package io.novafoundation.nova.feature_staking_api.domain.model.relaychain
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin
import io.novafoundation.nova.feature_staking_api.domain.model.Nominations
import io.novafoundation.nova.feature_staking_api.domain.model.ValidatorPrefs
import io.novafoundation.nova.runtime.ext.addressOf
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.runtime.AccountId
sealed class StakingState(
val chain: Chain,
val chainAsset: Chain.Asset,
) {
class NonStash(chain: Chain, chainAsset: Chain.Asset) : StakingState(chain, chainAsset)
sealed class Stash(
chain: Chain,
chainAsset: Chain.Asset,
val accountId: AccountId,
val controllerId: AccountId,
val stashId: AccountId
) : StakingState(chain, chainAsset) {
val accountAddress: String = chain.addressOf(accountId)
val stashAddress = chain.addressOf(stashId)
val controllerAddress = chain.addressOf(controllerId)
class None(
chain: Chain,
chainAsset: Chain.Asset,
accountId: AccountId,
controllerId: AccountId,
stashId: AccountId,
) : Stash(chain, chainAsset, accountId, controllerId, stashId)
class Validator(
chain: Chain,
chainAsset: Chain.Asset,
accountId: AccountId,
controllerId: AccountId,
stashId: AccountId,
val prefs: ValidatorPrefs,
) : Stash(chain, chainAsset, accountId, controllerId, stashId)
class Nominator(
chain: Chain,
chainAsset: Chain.Asset,
accountId: AccountId,
controllerId: AccountId,
stashId: AccountId,
val nominations: Nominations,
) : Stash(chain, chainAsset, accountId, controllerId, stashId)
}
}
fun StakingState.Stash.stashTransactionOrigin(): TransactionOrigin = TransactionOrigin.WalletWithAccount(stashId)
fun StakingState.Stash.controllerTransactionOrigin(): TransactionOrigin = TransactionOrigin.WalletWithAccount(controllerId)
fun StakingState.Stash.accountIsStash(): Boolean = accountId.contentEquals(stashId)
@@ -0,0 +1,10 @@
package io.novafoundation.nova.feature_staking_api.domain.nominationPool.model
import java.math.BigInteger
@JvmInline
value class PoolId(val value: PoolIdRaw)
typealias PoolIdRaw = BigInteger
fun PoolId(id: Int) = PoolId(id.toBigInteger())
@@ -0,0 +1,17 @@
package io.novafoundation.nova.feature_staking_api.presentation.nominationPools.display
import io.novafoundation.nova.common.utils.images.Icon
import io.novafoundation.nova.common.view.TableCellView
import io.novasama.substrate_sdk_android.runtime.AccountId
class PoolDisplayModel(
val icon: Icon,
val title: String,
val poolAccountId: AccountId,
val address: String
)
fun TableCellView.showPool(poolDisplayModel: PoolDisplayModel) {
loadImage(poolDisplayModel.icon)
showValue(poolDisplayModel.title)
}
@@ -0,0 +1,8 @@
package io.novafoundation.nova.feature_staking_api.presentation.nominationPools.display
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
interface PoolDisplayUseCase {
suspend fun getPoolDisplay(poolId: Int, chain: Chain): PoolDisplayModel
}