Skip to content

Commit

Permalink
Merge pull request #1787 from novasamatech/myth_staking/redeem
Browse files Browse the repository at this point in the history
Redeem flow
  • Loading branch information
valentunn authored Feb 10, 2025
2 parents a5e3de4 + cf3b0d2 commit 1111003
Show file tree
Hide file tree
Showing 30 changed files with 671 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import io.novafoundation.nova.app.root.navigation.navigators.staking.mythos.Sele
import io.novafoundation.nova.app.root.navigation.navigators.staking.mythos.SelectMythosCollatorInterScreenCommunicatorImpl
import io.novafoundation.nova.common.di.scope.ApplicationScope
import io.novafoundation.nova.feature_staking_impl.presentation.MythosStakingRouter
import io.novafoundation.nova.feature_staking_impl.presentation.StakingDashboardRouter
import io.novafoundation.nova.feature_staking_impl.presentation.mythos.SelectMythosInterScreenCommunicator
import io.novafoundation.nova.feature_staking_impl.presentation.mythos.start.selectCollatorSettings.SelectMythCollatorSettingsInterScreenCommunicator

Expand All @@ -18,8 +19,9 @@ class MythosStakingNavigationModule {
@ApplicationScope
fun provideMythosStakingRouter(
navigationHoldersRegistry: NavigationHoldersRegistry,
stakingDashboardRouter: StakingDashboardRouter,
): MythosStakingRouter {
return MythosStakingNavigator(navigationHoldersRegistry)
return MythosStakingNavigator(navigationHoldersRegistry, stakingDashboardRouter)
}

@Provides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package io.novafoundation.nova.app.root.navigation.navigators.staking.mythos
import io.novafoundation.nova.app.R
import io.novafoundation.nova.app.root.navigation.navigators.BaseNavigator
import io.novafoundation.nova.app.root.navigation.navigators.NavigationHoldersRegistry
import io.novafoundation.nova.feature_staking_impl.domain.staking.redeem.RedeemConsequences
import io.novafoundation.nova.feature_staking_impl.presentation.MythosStakingRouter
import io.novafoundation.nova.feature_staking_impl.presentation.StakingDashboardRouter
import io.novafoundation.nova.feature_staking_impl.presentation.mythos.start.confirm.ConfirmStartMythosStakingFragment
import io.novafoundation.nova.feature_staking_impl.presentation.mythos.start.confirm.ConfirmStartMythosStakingPayload
import io.novafoundation.nova.feature_staking_impl.presentation.mythos.unbond.confirm.ConfirmUnbondMythosFragment
Expand All @@ -13,6 +15,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.validators.detai

class MythosStakingNavigator(
navigationHoldersRegistry: NavigationHoldersRegistry,
private val stakingDashboardRouter: StakingDashboardRouter,
) : BaseNavigator(navigationHoldersRegistry), MythosStakingRouter {

override fun openCollatorDetails(payload: StakeTargetDetailsPayload) {
Expand Down Expand Up @@ -53,7 +56,17 @@ class MythosStakingNavigator(
}

override fun openRedeem() {
// TODO
navigationBuilder()
.action(R.id.action_stakingFragment_to_mythosRedeemFragment)
.navigateInFirstAttachedContext()
}

override fun finishRedeemFlow(redeemConsequences: RedeemConsequences) {
if (redeemConsequences.willKillStash) {
stakingDashboardRouter.returnToStakingDashboard()
} else {
returnToStakingMain()
}
}

override fun returnToStartStaking() {
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/res/navigation/staking_main_graph.xml
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,14 @@
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />

<action
android:id="@+id/action_stakingFragment_to_mythosRedeemFragment"
app:destination="@id/mythosRedeemFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
</fragment>

<action
Expand Down Expand Up @@ -531,4 +539,9 @@
<include app:graph="@navigation/staking_mythos_start_graph" />

<include app:graph="@navigation/staking_mythos_unbond_graph" />

<fragment
android:id="@+id/mythosRedeemFragment"
android:name="io.novafoundation.nova.feature_staking_impl.presentation.mythos.redeem.MythosRedeemFragment"
android:label="MythosRedeemFragment" />
</navigation>
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.novafoundation.nova.common.presentation

import io.novafoundation.nova.common.domain.ExtendedLoadingState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
Expand All @@ -26,6 +27,13 @@ fun <T> LoadingView<T>.showLoadingState(loadingState: LoadingState<T>) {
}
}

fun <T> LoadingView<T>.showLoadingState(loadingState: ExtendedLoadingState<T>) {
when (loadingState) {
is ExtendedLoadingState.Loaded -> showData(loadingState.data)
is ExtendedLoadingState.Loading, is ExtendedLoadingState.Error -> showLoading()
}
}

@Suppress("UNCHECKED_CAST")
inline fun <T, R> LoadingState<T>.map(mapper: (T) -> R): LoadingState<R> {
return when (this) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ abstract class LockDao {
@Query("SELECT * FROM locks WHERE metaId = :metaId AND chainId = :chainId AND assetId = :chainAssetId")
abstract fun observeBalanceLocks(metaId: Long, chainId: String, chainAssetId: Int): Flow<List<BalanceLockLocal>>

@Query("SELECT * FROM locks WHERE metaId = :metaId AND chainId = :chainId AND assetId = :chainAssetId")
abstract suspend fun getBalanceLocks(metaId: Long, chainId: String, chainAssetId: Int): List<BalanceLockLocal>

@Query(
"""
SELECT * FROM locks
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ fun CollatorStakingCalls.unlock(amount: Balance) {
)
}

fun CollatorStakingCalls.release() {
builder.call(
moduleName = Modules.COLLATOR_STAKING,
callName = "release",
arguments = emptyMap()
)
}

private fun StakingIntent.toEncodableInstance(): Any {
return structOf(
"candidate" to candidate.value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,22 @@ import io.novafoundation.nova.common.data.network.runtime.binding.bindBlockNumbe
import io.novafoundation.nova.common.data.network.runtime.binding.bindList
import io.novafoundation.nova.common.data.network.runtime.binding.bindNumber
import io.novafoundation.nova.common.data.network.runtime.binding.castToStruct
import io.novafoundation.nova.common.utils.sumByBigInteger
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance

class MythReleaseRequest(
val block: BlockNumber,
val amount: Balance
)

fun MythReleaseRequest.isRedeemableAt(at: BlockNumber): Boolean {
return at >= block
}

fun List<MythReleaseRequest>.totalRedeemable(at: BlockNumber): Balance {
return sumByBigInteger { if (it.isRedeemableAt(at)) it.amount else Balance.ZERO }
}

fun bindMythReleaseRequest(decoded: Any?): MythReleaseRequest {
val asStruct = decoded.castToStruct()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,20 @@ val MythosLocks.total: Balance
get() = releasing + staked

fun BalanceLocksRepository.observeMythosLocks(metaId: Long, chain: Chain, chainAsset: Chain.Asset): Flow<MythosLocks> {
return observeBalanceLocks(metaId, chain, chainAsset).map { locks ->
MythosLocks(
releasing = locks.findAmountOrZero(MythosStakingFreezeIds.RELEASING),
staked = locks.findAmountOrZero(MythosStakingFreezeIds.STAKING)
)
}.distinctUntilChanged()
return observeBalanceLocks(metaId, chain, chainAsset)
.map { locks -> locks.findMythosLocks() }
.distinctUntilChanged()
}

suspend fun BalanceLocksRepository.getMythosLocks(metaId: Long, chainAsset: Chain.Asset): MythosLocks {
return getBalanceLocks(metaId, chainAsset).findMythosLocks()
}

private fun List<BalanceLock>.findMythosLocks(): MythosLocks {
return MythosLocks(
releasing = findAmountOrZero(MythosStakingFreezeIds.RELEASING),
staked = findAmountOrZero(MythosStakingFreezeIds.STAKING)
)
}

private fun List<BalanceLock>.findAmountOrZero(id: BalanceLockId): Balance {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ interface MythosStakingRepository {
suspend fun maxDelegatorsPerCollator(chainId: ChainId): Int

suspend fun unstakeDurationInSessions(chainId: ChainId): Int

suspend fun maxReleaseRequests(chainId: ChainId): Int
}

@FeatureScope
Expand Down Expand Up @@ -67,4 +69,8 @@ class RealMythosStakingRepository @Inject constructor(
metadata.collatorStaking().numberConstant("StakeUnlockDelay").toInt()
}
}

override suspend fun maxReleaseRequests(chainId: ChainId): Int {
return maxCollatorsPerDelegator(chainId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ interface MythosUserStakeRepository {
chainId: ChainId,
accountId: AccountIdKey
): Flow<List<MythReleaseRequest>>

suspend fun releaseQueues(
chainId: ChainId,
accountId: AccountIdKey
): List<MythReleaseRequest>
}

@FeatureScope
Expand Down Expand Up @@ -84,6 +89,12 @@ class RealMythosUserStakeRepository @Inject constructor(
}
}

override suspend fun releaseQueues(chainId: ChainId, accountId: AccountIdKey): List<MythReleaseRequest> {
return localStorageDataSource.query(chainId) {
metadata.collatorStaking.releaseQueues.query(accountId.value).orEmpty()
}
}

private suspend fun RuntimeCallsApi.shouldClaimPendingRewards(accountId: AccountIdKey): Boolean {
return call(
section = "CollatorStakingApi",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import io.novafoundation.nova.feature_staking_impl.presentation.bagList.rebag.di
import io.novafoundation.nova.feature_staking_impl.presentation.dashboard.main.di.StakingDashboardComponent
import io.novafoundation.nova.feature_staking_impl.presentation.dashboard.more.di.MoreStakingOptionsComponent
import io.novafoundation.nova.feature_staking_impl.presentation.mythos.SelectMythosInterScreenCommunicator
import io.novafoundation.nova.feature_staking_impl.presentation.mythos.redeem.di.MythosRedeemComponent
import io.novafoundation.nova.feature_staking_impl.presentation.mythos.start.confirm.di.ConfirmStartMythosStakingComponent
import io.novafoundation.nova.feature_staking_impl.presentation.mythos.start.selectCollator.di.SelectMythosCollatorComponent
import io.novafoundation.nova.feature_staking_impl.presentation.mythos.start.selectCollatorSettings.SelectMythCollatorSettingsInterScreenCommunicator
Expand Down Expand Up @@ -258,6 +259,8 @@ interface StakingFeatureComponent : StakingFeatureApi {

fun confirmUnbondMythosFactory(): ConfirmUnbondMythosComponent.Factory

fun redeemMythosFactory(): MythosRedeemComponent.Factory

@Component.Factory
interface Factory {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import io.novafoundation.nova.feature_staking_impl.domain.mythos.main.stakeSumma
import io.novafoundation.nova.feature_staking_impl.domain.mythos.main.stakeSummary.RealMythosStakeSummaryInteractor
import io.novafoundation.nova.feature_staking_impl.domain.mythos.main.unbonding.MythosUnbondingInteractor
import io.novafoundation.nova.feature_staking_impl.domain.mythos.main.unbonding.RealMythosUnbondingInteractor
import io.novafoundation.nova.feature_staking_impl.domain.mythos.redeem.MythosRedeemInteractor
import io.novafoundation.nova.feature_staking_impl.domain.mythos.redeem.RealMythosRedeemInteractor
import io.novafoundation.nova.feature_staking_impl.domain.mythos.start.RealStartMythosStakingInteractor
import io.novafoundation.nova.feature_staking_impl.domain.mythos.start.StartMythosStakingInteractor
import io.novafoundation.nova.feature_staking_impl.domain.mythos.unbond.RealUnbondMythosStakingInteractor
Expand Down Expand Up @@ -56,6 +58,9 @@ interface MythosBindsModule {
@Binds
fun bindUnbondingInteractor(implementation: RealMythosUnbondingInteractor): MythosUnbondingInteractor

@Binds
fun bindRedeemInteractor(implementation: RealMythosRedeemInteractor): MythosRedeemInteractor

@Binds
fun bindStartStakingInteractor(implementation: RealStartMythosStakingInteractor): StartMythosStakingInteractor

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import dagger.Provides
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.validation.ValidationSystem
import io.novafoundation.nova.feature_staking_impl.domain.mythos.common.validations.MythosNoPendingRewardsValidationFactory
import io.novafoundation.nova.feature_staking_impl.domain.mythos.redeem.validations.RedeemMythosValidationSystem
import io.novafoundation.nova.feature_staking_impl.domain.mythos.redeem.validations.mythosRedeem
import io.novafoundation.nova.feature_staking_impl.domain.mythos.start.validations.MythosMinimumDelegationValidationFactory
import io.novafoundation.nova.feature_staking_impl.domain.mythos.start.validations.StartMythosStakingValidationSystem
import io.novafoundation.nova.feature_staking_impl.domain.mythos.start.validations.mythosStakingStart
import io.novafoundation.nova.feature_staking_impl.domain.mythos.unbond.validations.MythosReleaseRequestLimitNotReachedValidationFactory
import io.novafoundation.nova.feature_staking_impl.domain.mythos.unbond.validations.UnbondMythosValidationSystem
import io.novafoundation.nova.feature_staking_impl.domain.mythos.unbond.validations.mythosUnbond

Expand All @@ -26,8 +29,15 @@ class MythosModule {
@Provides
@FeatureScope
fun provideUnbondValidationSystem(
hasPendingRewardsValidationFactory: MythosNoPendingRewardsValidationFactory
hasPendingRewardsValidationFactory: MythosNoPendingRewardsValidationFactory,
releaseRequestLimitNotReachedValidation: MythosReleaseRequestLimitNotReachedValidationFactory
): UnbondMythosValidationSystem {
return ValidationSystem.mythosUnbond(hasPendingRewardsValidationFactory)
return ValidationSystem.mythosUnbond(hasPendingRewardsValidationFactory, releaseRequestLimitNotReachedValidation)
}

@Provides
@FeatureScope
fun provideRedeemValidationSystem(): RedeemMythosValidationSystem {
return ValidationSystem.mythosRedeem()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import io.novafoundation.nova.common.address.intoKey
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.utils.flowOfAll
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn
import io.novafoundation.nova.feature_account_api.domain.interfaces.requireIdOfSelectedMetaAccountIn
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.mythos.network.blockchain.model.MythReleaseRequest
Expand Down Expand Up @@ -42,8 +42,7 @@ class RealMythosUnbondingInteractor @Inject constructor(
val chainId = stakingOption.chain.id

return flowOfAll {
val metaAccount = accountRepository.getSelectedMetaAccount()
val accountId = metaAccount.requireAccountIdIn(stakingOption.chain).intoKey()
val accountId = accountRepository.requireIdOfSelectedMetaAccountIn(stakingOption.chain).intoKey()

combine(
userStakeRepository.releaseQueuesFlow(chainId, accountId),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package io.novafoundation.nova.feature_staking_impl.domain.mythos.redeem

import io.novafoundation.nova.common.address.intoKey
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.utils.flowOfAll
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
import io.novafoundation.nova.feature_account_api.data.extrinsic.execution.requireOk
import io.novafoundation.nova.feature_account_api.data.model.Fee
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_account_api.domain.interfaces.requireIdOfSelectedMetaAccountIn
import io.novafoundation.nova.feature_staking_impl.data.StakingSharedState
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.calls.collatorStaking
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.calls.release
import io.novafoundation.nova.feature_staking_impl.data.mythos.network.blockchain.model.totalRedeemable
import io.novafoundation.nova.feature_staking_impl.data.mythos.repository.MythosUserStakeRepository
import io.novafoundation.nova.feature_staking_impl.data.mythos.repository.getMythosLocks
import io.novafoundation.nova.feature_staking_impl.data.mythos.repository.total
import io.novafoundation.nova.feature_staking_impl.domain.staking.redeem.RedeemConsequences
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
import io.novafoundation.nova.feature_wallet_api.data.repository.BalanceLocksRepository
import io.novafoundation.nova.runtime.repository.ChainStateRepository
import io.novafoundation.nova.runtime.state.chain
import io.novafoundation.nova.runtime.state.chainAndAsset
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import javax.inject.Inject

interface MythosRedeemInteractor {

fun redeemAmountFlow(): Flow<Balance>

suspend fun estimateFee(): Fee

suspend fun redeem(redeemAmount: Balance): Result<RedeemConsequences>
}

@FeatureScope
class RealMythosRedeemInteractor @Inject constructor(
private val userStakeRepository: MythosUserStakeRepository,
private val chainStateRepository: ChainStateRepository,
private val stakingSharedState: StakingSharedState,
private val accountRepository: AccountRepository,
private val extrinsicService: ExtrinsicService,
private val balanceLocksRepository: BalanceLocksRepository,
) : MythosRedeemInteractor {

override fun redeemAmountFlow(): Flow<Balance> {
return flowOfAll {
val chain = stakingSharedState.chain()
val accountId = accountRepository.requireIdOfSelectedMetaAccountIn(chain).intoKey()

combine(
userStakeRepository.releaseQueuesFlow(chain.id, accountId),
chainStateRepository.currentBlockNumberFlow(chain.id)
) { releaseRequests, blockNumber ->
releaseRequests.totalRedeemable(at = blockNumber)
}
}
}

override suspend fun estimateFee(): Fee {
val chain = stakingSharedState.chain()

return extrinsicService.estimateFee(chain, TransactionOrigin.SelectedWallet) {
collatorStaking.release()
}
}

override suspend fun redeem(redeemAmount: Balance): Result<RedeemConsequences> {
val (chain, chainAsset) = stakingSharedState.chainAndAsset()
val metaAccount = accountRepository.getSelectedMetaAccount()
val mythStakingFreezes = balanceLocksRepository.getMythosLocks(metaAccount.id, chainAsset)

return extrinsicService.submitExtrinsicAndAwaitExecution(chain, TransactionOrigin.SelectedWallet) {
collatorStaking.release()
}
.requireOk()
.map {
val redeemedAll = mythStakingFreezes.total == redeemAmount
RedeemConsequences(willKillStash = redeemedAll)
}
}
}
Loading

0 comments on commit 1111003

Please sign in to comment.