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.events.StakingAlertUM
|
||||
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.StakingFeeLoader
|
||||
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 getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
|
||||
private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase,
|
||||
private val getEffectiveStakingFee: GetEffectiveStakingFee,
|
||||
private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase,
|
||||
private val isAnyTokenStakedUseCase: IsAnyTokenStakedUseCase,
|
||||
private val invalidatePendingTransactionsUseCase: InvalidatePendingTransactionsUseCase,
|
||||
|
|
@ -945,9 +947,16 @@ internal class StakingModel @Inject constructor(
|
|||
null
|
||||
}
|
||||
|
||||
val effectiveFee = getEffectiveStakingFee(
|
||||
stakeKitFee = fee,
|
||||
amount = amount,
|
||||
userWallet = userWallet,
|
||||
feeCurrencyStatus = feeStatus,
|
||||
)
|
||||
|
||||
val balanceAfterTransaction = calculateBalanceAfterTransaction(
|
||||
amount = amount.orZero(),
|
||||
fee = fee.orZero(),
|
||||
fee = effectiveFee.orZero(),
|
||||
reduceAmountBy = confirmationState?.reduceAmountBy.orZero(),
|
||||
actionType = value.actionType,
|
||||
)
|
||||
|
|
@ -956,9 +965,12 @@ internal class StakingModel @Inject constructor(
|
|||
currencyStatus = cryptoCurrencyStatus,
|
||||
feeCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
amount = amount,
|
||||
fee = fee,
|
||||
fee = effectiveFee,
|
||||
feeCurrencyBalanceAfterTransaction = balanceAfterTransaction,
|
||||
)
|
||||
|
||||
val hasRentWarning = currencyStatus.rentWarning != null
|
||||
|
||||
stateController.update(
|
||||
AddStakingNotificationsTransformer(
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
|
|
@ -968,8 +980,8 @@ internal class StakingModel @Inject constructor(
|
|||
currencyWarning = currencyWarning,
|
||||
currencyCheck = currencyStatus,
|
||||
isSubtractAvailable = isAmountSubtractAvailable,
|
||||
feeError = feeError,
|
||||
stakingError = stakingError,
|
||||
feeError = if (hasRentWarning) null else feeError,
|
||||
stakingError = if (hasRentWarning) null else stakingError,
|
||||
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.StakingStep
|
||||
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.StakingOperationsFactory
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
|
|
@ -92,6 +93,7 @@ internal abstract class StakingModelTestBase {
|
|||
protected val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase = mockk()
|
||||
protected val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase = mockk()
|
||||
protected val getCurrencyCheckUseCase: GetCurrencyCheckUseCase = mockk()
|
||||
protected val getEffectiveStakingFee: GetEffectiveStakingFee = mockk()
|
||||
protected val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase = mockk()
|
||||
protected val isAnyTokenStakedUseCase: IsAnyTokenStakedUseCase = mockk()
|
||||
private val invalidatePendingTransactionsUseCase: InvalidatePendingTransactionsUseCase = mockk()
|
||||
|
|
@ -145,6 +147,9 @@ internal abstract class StakingModelTestBase {
|
|||
coEvery {
|
||||
isAmountSubtractAvailableUseCase(testUserWalletId, any())
|
||||
} returns Either.Right(false)
|
||||
coEvery {
|
||||
getEffectiveStakingFee(any(), any(), any(), any())
|
||||
} answers { firstArg() }
|
||||
every { getActionsUseCase(testUserWalletId, any()) } returns emptyFlow()
|
||||
every { getBalanceHidingSettingsUseCase() } returns emptyFlow()
|
||||
mockBalanceUpdater = mockk {
|
||||
|
|
@ -178,6 +183,7 @@ internal abstract class StakingModelTestBase {
|
|||
saveBlockchainErrorUseCase = saveBlockchainErrorUseCase,
|
||||
getBalanceNotEnoughForFeeWarningUseCase = getBalanceNotEnoughForFeeWarningUseCase,
|
||||
getCurrencyCheckUseCase = getCurrencyCheckUseCase,
|
||||
getEffectiveStakingFee = getEffectiveStakingFee,
|
||||
isAmountSubtractAvailableUseCase = isAmountSubtractAvailableUseCase,
|
||||
isAnyTokenStakedUseCase = isAnyTokenStakedUseCase,
|
||||
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