Updated on 2026-08-14
This commit is contained in:
parent
a236e11593
commit
1e08fd51ee
4 changed files with 202 additions and 4 deletions
|
|
@ -76,6 +76,7 @@ import com.tangem.features.staking.impl.presentation.state.*
|
||||||
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
|
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
|
||||||
import com.tangem.features.staking.impl.presentation.state.events.StakingAlertUM
|
import com.tangem.features.staking.impl.presentation.state.events.StakingAlertUM
|
||||||
import com.tangem.features.staking.impl.presentation.state.events.StakingEventFactory
|
import com.tangem.features.staking.impl.presentation.state.events.StakingEventFactory
|
||||||
|
import com.tangem.features.staking.impl.presentation.state.helpers.GetEffectiveStakingFee
|
||||||
import com.tangem.features.staking.impl.presentation.state.helpers.StakingBalanceUpdater
|
import com.tangem.features.staking.impl.presentation.state.helpers.StakingBalanceUpdater
|
||||||
import com.tangem.features.staking.impl.presentation.state.helpers.StakingFeeLoader
|
import com.tangem.features.staking.impl.presentation.state.helpers.StakingFeeLoader
|
||||||
import com.tangem.features.staking.impl.presentation.state.helpers.StakingOperationsFactory
|
import com.tangem.features.staking.impl.presentation.state.helpers.StakingOperationsFactory
|
||||||
|
|
@ -131,6 +132,7 @@ internal class StakingModel @Inject constructor(
|
||||||
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase,
|
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase,
|
||||||
private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
|
private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
|
||||||
private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase,
|
private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase,
|
||||||
|
private val getEffectiveStakingFee: GetEffectiveStakingFee,
|
||||||
private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase,
|
private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase,
|
||||||
private val isAnyTokenStakedUseCase: IsAnyTokenStakedUseCase,
|
private val isAnyTokenStakedUseCase: IsAnyTokenStakedUseCase,
|
||||||
private val invalidatePendingTransactionsUseCase: InvalidatePendingTransactionsUseCase,
|
private val invalidatePendingTransactionsUseCase: InvalidatePendingTransactionsUseCase,
|
||||||
|
|
@ -945,9 +947,16 @@ internal class StakingModel @Inject constructor(
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val effectiveFee = getEffectiveStakingFee(
|
||||||
|
stakeKitFee = fee,
|
||||||
|
amount = amount,
|
||||||
|
userWallet = userWallet,
|
||||||
|
feeCurrencyStatus = feeStatus,
|
||||||
|
)
|
||||||
|
|
||||||
val balanceAfterTransaction = calculateBalanceAfterTransaction(
|
val balanceAfterTransaction = calculateBalanceAfterTransaction(
|
||||||
amount = amount.orZero(),
|
amount = amount.orZero(),
|
||||||
fee = fee.orZero(),
|
fee = effectiveFee.orZero(),
|
||||||
reduceAmountBy = confirmationState?.reduceAmountBy.orZero(),
|
reduceAmountBy = confirmationState?.reduceAmountBy.orZero(),
|
||||||
actionType = value.actionType,
|
actionType = value.actionType,
|
||||||
)
|
)
|
||||||
|
|
@ -956,9 +965,12 @@ internal class StakingModel @Inject constructor(
|
||||||
currencyStatus = cryptoCurrencyStatus,
|
currencyStatus = cryptoCurrencyStatus,
|
||||||
feeCurrencyStatus = feeCryptoCurrencyStatus,
|
feeCurrencyStatus = feeCryptoCurrencyStatus,
|
||||||
amount = amount,
|
amount = amount,
|
||||||
fee = fee,
|
fee = effectiveFee,
|
||||||
feeCurrencyBalanceAfterTransaction = balanceAfterTransaction,
|
feeCurrencyBalanceAfterTransaction = balanceAfterTransaction,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val hasRentWarning = currencyStatus.rentWarning != null
|
||||||
|
|
||||||
stateController.update(
|
stateController.update(
|
||||||
AddStakingNotificationsTransformer(
|
AddStakingNotificationsTransformer(
|
||||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||||
|
|
@ -968,8 +980,8 @@ internal class StakingModel @Inject constructor(
|
||||||
currencyWarning = currencyWarning,
|
currencyWarning = currencyWarning,
|
||||||
currencyCheck = currencyStatus,
|
currencyCheck = currencyStatus,
|
||||||
isSubtractAvailable = isAmountSubtractAvailable,
|
isSubtractAvailable = isAmountSubtractAvailable,
|
||||||
feeError = feeError,
|
feeError = if (hasRentWarning) null else feeError,
|
||||||
stakingError = stakingError,
|
stakingError = if (hasRentWarning) null else stakingError,
|
||||||
integration = integration,
|
integration = integration,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
package com.tangem.features.staking.impl.presentation.state.helpers
|
||||||
|
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||||
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
|
import com.tangem.domain.transaction.usecase.EstimateFeeUseCase
|
||||||
|
import com.tangem.lib.crypto.BlockchainUtils
|
||||||
|
import com.tangem.utils.extensions.orZero
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the fee used by the rent-exemption check ([REDACTED_TASK_KEY]).
|
||||||
|
*
|
||||||
|
* When StakeKit provides a fee, returns it as-is. When the StakeKit fee is unavailable (`null`) —
|
||||||
|
* which happens on Solana when the post-transaction balance would drop below the rent-exempt minimum
|
||||||
|
* and StakeKit fails its fee calculation — falls back to a client-side Solana fee estimate so the
|
||||||
|
* rent-exemption check can still run independently of StakeKit. Returns `null` for non-Solana
|
||||||
|
* networks or when estimation fails (in those cases the rent check stays inert, preserving previous
|
||||||
|
* behavior).
|
||||||
|
*/
|
||||||
|
internal class GetEffectiveStakingFee @Inject constructor(
|
||||||
|
private val estimateFeeUseCase: EstimateFeeUseCase,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend operator fun invoke(
|
||||||
|
stakeKitFee: BigDecimal?,
|
||||||
|
amount: BigDecimal?,
|
||||||
|
userWallet: UserWallet,
|
||||||
|
feeCurrencyStatus: CryptoCurrencyStatus?,
|
||||||
|
): BigDecimal? {
|
||||||
|
if (stakeKitFee != null) return stakeKitFee
|
||||||
|
if (feeCurrencyStatus == null) return null
|
||||||
|
if (!BlockchainUtils.isSolana(feeCurrencyStatus.currency.network.rawId)) return null
|
||||||
|
|
||||||
|
return estimateFeeUseCase(
|
||||||
|
amount = amount.orZero(),
|
||||||
|
userWallet = userWallet,
|
||||||
|
cryptoCurrencyStatus = feeCurrencyStatus,
|
||||||
|
).getOrNull()?.normal?.amount?.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -38,6 +38,7 @@ import com.tangem.features.staking.impl.navigation.InnerStakingRouter
|
||||||
import com.tangem.features.staking.impl.presentation.state.StakingStateController
|
import com.tangem.features.staking.impl.presentation.state.StakingStateController
|
||||||
import com.tangem.features.staking.impl.presentation.state.StakingStep
|
import com.tangem.features.staking.impl.presentation.state.StakingStep
|
||||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||||
|
import com.tangem.features.staking.impl.presentation.state.helpers.GetEffectiveStakingFee
|
||||||
import com.tangem.features.staking.impl.presentation.state.helpers.StakingBalanceUpdater
|
import com.tangem.features.staking.impl.presentation.state.helpers.StakingBalanceUpdater
|
||||||
import com.tangem.features.staking.impl.presentation.state.helpers.StakingOperationsFactory
|
import com.tangem.features.staking.impl.presentation.state.helpers.StakingOperationsFactory
|
||||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||||
|
|
@ -92,6 +93,7 @@ internal abstract class StakingModelTestBase {
|
||||||
protected val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase = mockk()
|
protected val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase = mockk()
|
||||||
protected val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase = mockk()
|
protected val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase = mockk()
|
||||||
protected val getCurrencyCheckUseCase: GetCurrencyCheckUseCase = mockk()
|
protected val getCurrencyCheckUseCase: GetCurrencyCheckUseCase = mockk()
|
||||||
|
protected val getEffectiveStakingFee: GetEffectiveStakingFee = mockk()
|
||||||
protected val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase = mockk()
|
protected val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase = mockk()
|
||||||
protected val isAnyTokenStakedUseCase: IsAnyTokenStakedUseCase = mockk()
|
protected val isAnyTokenStakedUseCase: IsAnyTokenStakedUseCase = mockk()
|
||||||
private val invalidatePendingTransactionsUseCase: InvalidatePendingTransactionsUseCase = mockk()
|
private val invalidatePendingTransactionsUseCase: InvalidatePendingTransactionsUseCase = mockk()
|
||||||
|
|
@ -145,6 +147,9 @@ internal abstract class StakingModelTestBase {
|
||||||
coEvery {
|
coEvery {
|
||||||
isAmountSubtractAvailableUseCase(testUserWalletId, any())
|
isAmountSubtractAvailableUseCase(testUserWalletId, any())
|
||||||
} returns Either.Right(false)
|
} returns Either.Right(false)
|
||||||
|
coEvery {
|
||||||
|
getEffectiveStakingFee(any(), any(), any(), any())
|
||||||
|
} answers { firstArg() }
|
||||||
every { getActionsUseCase(testUserWalletId, any()) } returns emptyFlow()
|
every { getActionsUseCase(testUserWalletId, any()) } returns emptyFlow()
|
||||||
every { getBalanceHidingSettingsUseCase() } returns emptyFlow()
|
every { getBalanceHidingSettingsUseCase() } returns emptyFlow()
|
||||||
mockBalanceUpdater = mockk {
|
mockBalanceUpdater = mockk {
|
||||||
|
|
@ -178,6 +183,7 @@ internal abstract class StakingModelTestBase {
|
||||||
saveBlockchainErrorUseCase = saveBlockchainErrorUseCase,
|
saveBlockchainErrorUseCase = saveBlockchainErrorUseCase,
|
||||||
getBalanceNotEnoughForFeeWarningUseCase = getBalanceNotEnoughForFeeWarningUseCase,
|
getBalanceNotEnoughForFeeWarningUseCase = getBalanceNotEnoughForFeeWarningUseCase,
|
||||||
getCurrencyCheckUseCase = getCurrencyCheckUseCase,
|
getCurrencyCheckUseCase = getCurrencyCheckUseCase,
|
||||||
|
getEffectiveStakingFee = getEffectiveStakingFee,
|
||||||
isAmountSubtractAvailableUseCase = isAmountSubtractAvailableUseCase,
|
isAmountSubtractAvailableUseCase = isAmountSubtractAvailableUseCase,
|
||||||
isAnyTokenStakedUseCase = isAnyTokenStakedUseCase,
|
isAnyTokenStakedUseCase = isAnyTokenStakedUseCase,
|
||||||
invalidatePendingTransactionsUseCase = invalidatePendingTransactionsUseCase,
|
invalidatePendingTransactionsUseCase = invalidatePendingTransactionsUseCase,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
package com.tangem.features.staking.impl.presentation.state.helpers
|
||||||
|
|
||||||
|
import arrow.core.left
|
||||||
|
import arrow.core.right
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||||
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
|
import com.tangem.domain.transaction.error.GetFeeError
|
||||||
|
import com.tangem.domain.transaction.usecase.EstimateFeeUseCase
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
internal class GetEffectiveStakingFeeTest {
|
||||||
|
|
||||||
|
private val estimateFeeUseCase: EstimateFeeUseCase = mockk()
|
||||||
|
private val userWallet: UserWallet = mockk(relaxed = true)
|
||||||
|
|
||||||
|
private val getEffectiveStakingFee = GetEffectiveStakingFee(estimateFeeUseCase)
|
||||||
|
|
||||||
|
private fun feeStatus(rawId: String): CryptoCurrencyStatus = mockk {
|
||||||
|
every { currency.network.rawId } returns rawId
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN stakekit fee present WHEN invoke THEN returns it without estimating`() = runTest {
|
||||||
|
// Act
|
||||||
|
val result = getEffectiveStakingFee(
|
||||||
|
stakeKitFee = BigDecimal("0.5"),
|
||||||
|
amount = BigDecimal.ONE,
|
||||||
|
userWallet = userWallet,
|
||||||
|
feeCurrencyStatus = feeStatus("solana"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(result).isEqualTo(BigDecimal("0.5"))
|
||||||
|
coVerify(exactly = 0) { estimateFeeUseCase(any(), any(), any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN null stakekit fee AND solana WHEN invoke THEN returns client estimate`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val feeCurrencyStatus = feeStatus("solana")
|
||||||
|
val estimated = BigDecimal("0.000005")
|
||||||
|
val txFee: TransactionFee = mockk {
|
||||||
|
every { normal.amount.value } returns estimated
|
||||||
|
}
|
||||||
|
coEvery { estimateFeeUseCase(any(), userWallet, feeCurrencyStatus) } returns txFee.right()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = getEffectiveStakingFee(
|
||||||
|
stakeKitFee = null,
|
||||||
|
amount = BigDecimal.ZERO,
|
||||||
|
userWallet = userWallet,
|
||||||
|
feeCurrencyStatus = feeCurrencyStatus,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(result).isEqualTo(estimated)
|
||||||
|
coVerify(exactly = 1) { estimateFeeUseCase(BigDecimal.ZERO, userWallet, feeCurrencyStatus) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN null stakekit fee AND non-solana WHEN invoke THEN returns null without estimating`() = runTest {
|
||||||
|
// Act
|
||||||
|
val result = getEffectiveStakingFee(
|
||||||
|
stakeKitFee = null,
|
||||||
|
amount = BigDecimal.ZERO,
|
||||||
|
userWallet = userWallet,
|
||||||
|
feeCurrencyStatus = feeStatus("ethereum"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(result).isNull()
|
||||||
|
coVerify(exactly = 0) { estimateFeeUseCase(any(), any(), any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN null stakekit fee AND solana AND estimation fails WHEN invoke THEN returns null`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val feeCurrencyStatus = feeStatus("solana")
|
||||||
|
coEvery { estimateFeeUseCase(any(), any(), any()) } returns GetFeeError.UnknownError.left()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = getEffectiveStakingFee(
|
||||||
|
stakeKitFee = null,
|
||||||
|
amount = BigDecimal.ZERO,
|
||||||
|
userWallet = userWallet,
|
||||||
|
feeCurrencyStatus = feeCurrencyStatus,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(result).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN null fee currency status WHEN invoke THEN returns null`() = runTest {
|
||||||
|
// Act
|
||||||
|
val result = getEffectiveStakingFee(
|
||||||
|
stakeKitFee = null,
|
||||||
|
amount = BigDecimal.ZERO,
|
||||||
|
userWallet = userWallet,
|
||||||
|
feeCurrencyStatus = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(result).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN null stakekit fee AND null amount AND solana WHEN invoke THEN estimates with zero amount`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val feeCurrencyStatus = feeStatus("solana")
|
||||||
|
val estimated = BigDecimal("0.000005")
|
||||||
|
val txFee: TransactionFee = mockk {
|
||||||
|
every { normal.amount.value } returns estimated
|
||||||
|
}
|
||||||
|
coEvery {
|
||||||
|
estimateFeeUseCase(BigDecimal.ZERO, userWallet, feeCurrencyStatus)
|
||||||
|
} returns txFee.right()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = getEffectiveStakingFee(
|
||||||
|
stakeKitFee = null,
|
||||||
|
amount = null,
|
||||||
|
userWallet = userWallet,
|
||||||
|
feeCurrencyStatus = feeCurrencyStatus,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(result).isEqualTo(estimated)
|
||||||
|
coVerify(exactly = 1) { estimateFeeUseCase(BigDecimal.ZERO, userWallet, feeCurrencyStatus) }
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue