Updated on 2026-08-14
This commit is contained in:
parent
f8a9cac801
commit
f72c0f448e
13 changed files with 409 additions and 116 deletions
|
|
@ -38,7 +38,6 @@ class AmountStateConverter(
|
|||
private val iconStateConverter: CryptoCurrencyToIconStateConverter,
|
||||
private val isBalanceHidden: Boolean,
|
||||
private val accountTitleUM: AccountTitleUM,
|
||||
private val isMaxButtonVisible: Boolean = true,
|
||||
) : Converter<AmountParameters, AmountState> {
|
||||
|
||||
private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
|
|
@ -73,7 +72,6 @@ class AmountStateConverter(
|
|||
amountTextField = amountFieldConverter.convert(value.value),
|
||||
isPrimaryButtonEnabled = false,
|
||||
appCurrency = appCurrency,
|
||||
isMaxButtonVisible = isMaxButtonVisible,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -24,7 +24,6 @@ sealed class AmountState {
|
|||
* @param isEditingDisabled indicated whether amount is editable
|
||||
* @param reduceAmountBy reduces amount to be sent by specified value
|
||||
* @param isIgnoreReduce ignores reduce amount value
|
||||
* @param isMaxButtonVisible indicates whether the "Max" button is shown
|
||||
*/
|
||||
data class Data(
|
||||
override val isPrimaryButtonEnabled: Boolean,
|
||||
|
|
@ -38,7 +37,6 @@ sealed class AmountState {
|
|||
val isEditingDisabled: Boolean = false,
|
||||
val reduceAmountBy: BigDecimal = BigDecimal.ZERO,
|
||||
val isIgnoreReduce: Boolean = false,
|
||||
val isMaxButtonVisible: Boolean = true,
|
||||
) : AmountState()
|
||||
|
||||
data object Empty : AmountState() {
|
||||
|
|
|
|||
|
|
@ -88,7 +88,6 @@ internal fun LazyListScope.amountFieldV2(
|
|||
@Composable
|
||||
private fun AmountInfo(amountUM: AmountState, onMaxAmountClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
val tokenIconState = (amountUM as? AmountState.Data)?.tokenIconState ?: CurrencyIconState.Loading
|
||||
val isMaxButtonVisible = (amountUM as? AmountState.Data)?.isMaxButtonVisible != false
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
|
|
@ -107,24 +106,22 @@ private fun AmountInfo(amountUM: AmountState, onMaxAmountClick: () -> Unit, modi
|
|||
amountUM = amountUM,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
if (isMaxButtonVisible) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.send_max_amount),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(TangemTheme.colors.button.secondary)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(),
|
||||
onClick = onMaxAmountClick,
|
||||
)
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp)
|
||||
.testTag(SendScreenTestTags.MAX_BUTTON),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.send_max_amount),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(TangemTheme.colors.button.secondary)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(),
|
||||
onClick = onMaxAmountClick,
|
||||
)
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp)
|
||||
.testTag(SendScreenTestTags.MAX_BUTTON),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
package com.tangem.common.ui.amountScreen.converters
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
|
||||
import com.tangem.common.ui.amountScreen.models.AmountParameters
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class AmountStateConverterTest {
|
||||
|
||||
private val currency = mockk<CryptoCurrency.Coin> {
|
||||
every { symbol } returns "ETH"
|
||||
every { decimals } returns 18
|
||||
every { name } returns "Ethereum"
|
||||
}
|
||||
private val status = CryptoCurrencyStatus(currency = currency, value = mockk(relaxed = true))
|
||||
private val iconStateConverter = mockk<CryptoCurrencyToIconStateConverter> {
|
||||
every { convert(any<CryptoCurrency>()) } returns CurrencyIconState.Loading
|
||||
}
|
||||
private val clickIntents = mockk<AmountScreenClickIntents>(relaxed = true)
|
||||
private val accountTitleUM = mockk<AccountTitleUM>()
|
||||
|
||||
private fun convert(isMaxButtonVisible: Boolean = true): AmountState = AmountStateConverter(
|
||||
clickIntents = clickIntents,
|
||||
appCurrency = AppCurrency.Default,
|
||||
cryptoCurrencyStatus = status,
|
||||
maxEnterAmount = EnterAmountBoundary(
|
||||
amount = BigDecimal.ONE,
|
||||
fiatAmount = BigDecimal.TEN,
|
||||
fiatRate = BigDecimal.ONE,
|
||||
),
|
||||
iconStateConverter = iconStateConverter,
|
||||
isBalanceHidden = false,
|
||||
accountTitleUM = accountTitleUM,
|
||||
isMaxButtonVisible = isMaxButtonVisible,
|
||||
).convert(AmountParameters(title = stringReference("Wallet"), value = ""))
|
||||
|
||||
@Test
|
||||
fun `GIVEN no isMaxButtonVisible param WHEN convert THEN Data isMaxButtonVisible is true`() {
|
||||
val result = convert()
|
||||
|
||||
assertThat((result as AmountState.Data).isMaxButtonVisible).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN isMaxButtonVisible false WHEN convert THEN Data isMaxButtonVisible is false`() {
|
||||
val result = convert(isMaxButtonVisible = false)
|
||||
|
||||
assertThat((result as AmountState.Data).isMaxButtonVisible).isFalse()
|
||||
}
|
||||
}
|
||||
|
|
@ -181,6 +181,19 @@ sealed class StakingAnalyticsEvent(
|
|||
AnalyticsParam.BLOCKCHAIN to blockchain,
|
||||
),
|
||||
)
|
||||
|
||||
data class SumLimitError(
|
||||
val token: String,
|
||||
val blockchain: String,
|
||||
val maxAmount: String,
|
||||
) : StakingAnalyticsEvent(
|
||||
event = "Error - Sum Limit",
|
||||
params = mapOf(
|
||||
AnalyticsParam.TOKEN_PARAM to token,
|
||||
AnalyticsParam.BLOCKCHAIN to blockchain,
|
||||
AnalyticsParam.ERROR_MESSAGE to "Maximum amount: $maxAmount",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
enum class StakeScreenSource {
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ import com.tangem.features.staking.impl.presentation.state.utils.withStubUnstake
|
|||
import com.tangem.lib.crypto.BlockchainUtils.isTon
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.*
|
||||
import com.tangem.utils.extensions.isPositive
|
||||
import com.tangem.utils.extensions.isSingleItem
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
|
@ -642,6 +643,31 @@ internal class StakingModel @Inject constructor(
|
|||
integration = integration,
|
||||
),
|
||||
)
|
||||
checkSumLimitExceeded()
|
||||
}
|
||||
|
||||
private fun checkSumLimitExceeded() {
|
||||
val maxLimit = (integration as? P2PEthPoolIntegration)
|
||||
?.enterArgs?.amountRequirement?.maximum
|
||||
?.takeIf { it.isPositive() }
|
||||
?: return
|
||||
|
||||
val enteredAmount = (uiState.value.amountState as? AmountState.Data)
|
||||
?.amountTextField?.cryptoAmount?.value
|
||||
?: return
|
||||
|
||||
if (enteredAmount > maxLimit) {
|
||||
// Pass the max limit as a stable crypto-formatted value (e.g. "0.15 ETH"); the event
|
||||
// builds the hardcoded English "Error Message" from it, so the model needs no resources.
|
||||
val formattedMax = maxLimit.format { crypto(cryptoCurrencyStatus.currency) }
|
||||
analyticsEventHandler.send(
|
||||
StakingAnalyticsEvent.SumLimitError(
|
||||
token = cryptoCurrencyStatus.currency.symbol,
|
||||
blockchain = cryptoCurrencyStatus.currency.network.name,
|
||||
maxAmount = formattedMax,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAmountPasteTriggerDismiss() {
|
||||
|
|
@ -658,6 +684,7 @@ internal class StakingModel @Inject constructor(
|
|||
integration = integration,
|
||||
),
|
||||
)
|
||||
checkSumLimitExceeded()
|
||||
}
|
||||
|
||||
override fun onCurrencyChangeClick(isFiat: Boolean) {
|
||||
|
|
@ -1099,7 +1126,12 @@ internal class StakingModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun showPrimaryClickAlert() {
|
||||
messageSender.send(StakingAlertUM.stakeMoreClickUnavailable(cryptoCurrencyStatus.currency))
|
||||
val message = if (integration is P2PEthPoolIntegration) {
|
||||
StakingAlertUM.stakeMoreClickUnavailableNoTargets()
|
||||
} else {
|
||||
StakingAlertUM.stakeMoreClickUnavailable(cryptoCurrencyStatus.currency)
|
||||
}
|
||||
messageSender.send(message)
|
||||
}
|
||||
|
||||
override fun onOpenLearnMoreAboutApproveClick() {
|
||||
|
|
|
|||
|
|
@ -54,6 +54,11 @@ internal object StakingAlertUM {
|
|||
),
|
||||
)
|
||||
|
||||
fun stakeMoreClickUnavailableNoTargets(): DialogMessage = DialogMessage(
|
||||
title = null,
|
||||
message = resourceReference(R.string.staking_no_validators_error_message),
|
||||
)
|
||||
|
||||
fun rewardsMinimumRequirementsError(cryptoCurrencyName: String, cryptoAmountValue: String): DialogMessage =
|
||||
DialogMessage(
|
||||
title = null,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import com.tangem.domain.models.account.Account
|
|||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.staking.StakingBalanceEntry
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.staking.model.P2PEthPoolIntegration
|
||||
import com.tangem.domain.staking.model.Period
|
||||
import com.tangem.domain.staking.model.StakingIntegration
|
||||
import com.tangem.domain.staking.model.StakingTarget
|
||||
|
|
@ -262,7 +261,6 @@ internal class SetInitialDataStateTransformer(
|
|||
walletTitle = stringReference(userWalletProvider().name),
|
||||
prefixText = resourceReference(R.string.common_from),
|
||||
).convert(account),
|
||||
isMaxButtonVisible = integration !is P2PEthPoolIntegration,
|
||||
).convert(
|
||||
AmountParameters(
|
||||
title = stringReference(userWalletProvider().name),
|
||||
|
|
|
|||
|
|
@ -96,12 +96,18 @@ internal class AmountRequirementStateTransformer(
|
|||
|
||||
return when (actionType) {
|
||||
is StakingActionCommonType.Enter -> {
|
||||
val enterRequirements = integration.enterArgs?.amountRequirement
|
||||
enterRequirements?.getError(amountDecimal, R.string.staking_amount_requirement_error)
|
||||
integration.enterArgs?.amountRequirement?.getError(
|
||||
amount = amountDecimal,
|
||||
minErrorRes = R.string.staking_amount_requirement_error,
|
||||
maxErrorRes = R.string.staking_max_amount_requirement_error,
|
||||
)
|
||||
}
|
||||
is StakingActionCommonType.Exit -> {
|
||||
val exitRequirements = integration.exitArgs?.amountRequirement
|
||||
exitRequirements?.getError(amountDecimal, R.string.staking_unstake_amount_requirement_error)
|
||||
integration.exitArgs?.amountRequirement?.getError(
|
||||
amount = amountDecimal,
|
||||
minErrorRes = R.string.staking_unstake_amount_requirement_error,
|
||||
maxErrorRes = R.string.staking_max_amount_requirement_error,
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
|
@ -118,31 +124,25 @@ internal class AmountRequirementStateTransformer(
|
|||
return isEnterOrExit && isTron && !isIntegerOnly
|
||||
}
|
||||
|
||||
private fun StakingAmountRequirement.getError(amount: BigDecimal, @StringRes errorTextRes: Int): TextReference? {
|
||||
private fun StakingAmountRequirement.getError(
|
||||
amount: BigDecimal,
|
||||
@StringRes minErrorRes: Int,
|
||||
@StringRes maxErrorRes: Int,
|
||||
): TextReference? {
|
||||
if (!isRequired) return null
|
||||
|
||||
val isExceedsMinRequirement = minimum?.compareTo(amount) == 1
|
||||
val isExceedsMaxRequirement = if (maximum?.isPositive() == true) {
|
||||
maximum?.compareTo(amount) == -1
|
||||
} else {
|
||||
maxAmount.amount?.compareTo(amount) == -1
|
||||
val effectiveMax = maximum?.takeIf { it.isPositive() } ?: maxAmount.amount
|
||||
val isExceedsMaxRequirement = effectiveMax?.compareTo(amount) == -1
|
||||
|
||||
val (errorRes, boundary) = when {
|
||||
isExceedsMinRequirement -> minErrorRes to minimum
|
||||
isExceedsMaxRequirement -> maxErrorRes to effectiveMax
|
||||
else -> return null
|
||||
}
|
||||
|
||||
val errorText = when {
|
||||
isExceedsMinRequirement -> {
|
||||
minimum.format {
|
||||
crypto(cryptoCurrencyStatus.currency)
|
||||
}
|
||||
}
|
||||
isExceedsMaxRequirement -> {
|
||||
maximum.format {
|
||||
crypto(cryptoCurrencyStatus.currency)
|
||||
}
|
||||
}
|
||||
else -> ""
|
||||
}
|
||||
return resourceReference(
|
||||
errorTextRes,
|
||||
wrappedList(errorText),
|
||||
).takeIf { isRequired && (isExceedsMinRequirement || isExceedsMaxRequirement) }
|
||||
val formatted = boundary.format { crypto(cryptoCurrencyStatus.currency) }
|
||||
return resourceReference(errorRes, wrappedList(formatted))
|
||||
}
|
||||
|
||||
data class Data(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,131 @@
|
|||
package com.tangem.features.staking.impl.presentation.model
|
||||
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||
import com.tangem.domain.staking.model.ethpool.VaultLimitInfo
|
||||
import com.tangem.domain.tokens.model.Amount
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStep
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Model-level tests for the P2P ETH pool staking integration:
|
||||
* verifies that [StakingAnalyticsEvent.SumLimitError] is sent when the entered amount
|
||||
* exceeds the vault's computed maximum (= limit − totalAssets).
|
||||
*
|
||||
* Fixture:
|
||||
* vault address = "0xabc" totalAssets = 5
|
||||
* limit "0xabc" limit = 10
|
||||
* → maximum = 10 − 5 = 5.0 (scale=1, RoundingMode.FLOOR)
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class StakingModelP2PSumLimitTest : StakingModelTestBase() {
|
||||
|
||||
override val testIntegrationId: StakingIntegrationID = StakingIntegrationID.P2PEthPool
|
||||
|
||||
private val vaultAddress = "0xabc"
|
||||
private val testVault = P2PEthPoolVault(
|
||||
vaultAddress = vaultAddress,
|
||||
displayName = "Test Vault",
|
||||
apy = BigDecimal("4.5"),
|
||||
baseApy = BigDecimal("4.0"),
|
||||
capacity = BigDecimal("1000"),
|
||||
totalAssets = BigDecimal("5"),
|
||||
feePercent = BigDecimal("0.1"),
|
||||
isPrivate = false,
|
||||
isGenesis = false,
|
||||
isSmoothingPool = false,
|
||||
isErc20 = false,
|
||||
tokenName = null,
|
||||
tokenSymbol = null,
|
||||
createdAt = 0L,
|
||||
)
|
||||
|
||||
// maximum = 10 − 5 = 5.0 (FLOOR scale=1)
|
||||
private val testLimits = mapOf(
|
||||
vaultAddress to VaultLimitInfo(limit = BigDecimal("10"), coefficient = null),
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun setUpP2P() {
|
||||
coEvery { p2pEthPoolRepository.getVaultsSync() } returns listOf(testVault)
|
||||
coEvery { p2pEthPoolRepository.getVaultLimitsSyncOrNull() } returns testLimits
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: returns a [MutableStateFlow] whose value has [amountState] set to an
|
||||
* [AmountState.Data] mock with the given [cryptoAmountValue].
|
||||
* The flow is also wired to [stateController.uiState].
|
||||
*/
|
||||
private fun stubUiStateWithCryptoAmount(cryptoAmountValue: BigDecimal): MutableStateFlow<StakingUiState> {
|
||||
val amountData = mockk<AmountState.Data>(relaxed = true) {
|
||||
every { amountTextField } returns mockk(relaxed = true) {
|
||||
every { cryptoAmount } returns Amount(
|
||||
currencySymbol = "ETH",
|
||||
value = cryptoAmountValue,
|
||||
decimals = 18,
|
||||
)
|
||||
}
|
||||
}
|
||||
val uiStateFlow = MutableStateFlow(
|
||||
mockk<StakingUiState>(relaxed = true) {
|
||||
every { currentStep } returns StakingStep.InitialInfo
|
||||
every { amountState } returns amountData
|
||||
},
|
||||
)
|
||||
every { stateController.uiState } returns uiStateFlow
|
||||
return uiStateFlow
|
||||
}
|
||||
|
||||
// ----- Test A ---------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun `GIVEN P2P vault max=5 WHEN amount 6 entered THEN SumLimitError analytics sent`() = runTest {
|
||||
stubUiStateWithCryptoAmount(BigDecimal("6"))
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onAmountValueChange("6")
|
||||
|
||||
verify {
|
||||
analyticsEventHandler.send(
|
||||
match { it is StakingAnalyticsEvent.SumLimitError }
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
// ----- Test B ---------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun `GIVEN P2P vault max=5 WHEN amount 4 entered THEN SumLimitError analytics NOT sent`() = runTest {
|
||||
stubUiStateWithCryptoAmount(BigDecimal("4"))
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onAmountValueChange("4")
|
||||
|
||||
verify(exactly = 0) {
|
||||
analyticsEventHandler.send(
|
||||
match { it is StakingAnalyticsEvent.SumLimitError }
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
}
|
||||
|
|
@ -57,8 +57,8 @@ internal abstract class StakingModelTestBase {
|
|||
|
||||
protected val testUserWalletId = UserWalletId("1234567890ABCDEF")
|
||||
protected val testCryptoCurrency: CryptoCurrency = mockk(relaxed = true)
|
||||
private val testIntegrationId = StakingIntegrationID.StakeKit.Coin.Solana
|
||||
private val testParams = StakingComponent.Params(
|
||||
protected open val testIntegrationId: StakingIntegrationID = StakingIntegrationID.StakeKit.Coin.Solana
|
||||
private val testParams get() = StakingComponent.Params(
|
||||
userWalletId = testUserWalletId,
|
||||
cryptoCurrency = testCryptoCurrency,
|
||||
integrationId = testIntegrationId,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.events
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.staking.impl.R
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class StakingAlertUMTest {
|
||||
|
||||
@Test
|
||||
fun `noTargets dialog uses no validators string and has no title`() {
|
||||
val message = StakingAlertUM.stakeMoreClickUnavailableNoTargets()
|
||||
|
||||
assertThat(message.title).isNull()
|
||||
assertThat((message.message as TextReference.Res).id)
|
||||
.isEqualTo(R.string.staking_no_validators_error_message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `default stake more dialog uses stake more unavailability string`() {
|
||||
val currency: CryptoCurrency = mockk(relaxed = true)
|
||||
|
||||
val message = StakingAlertUM.stakeMoreClickUnavailable(currency)
|
||||
|
||||
assertThat(message.title).isNull()
|
||||
assertThat((message.message as TextReference.Res).id)
|
||||
.isEqualTo(R.string.staking_stake_more_button_unavailability_reason)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers.amount
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.staking.model.StakingIntegration
|
||||
import com.tangem.domain.staking.model.common.StakingActionArgs
|
||||
import com.tangem.domain.staking.model.common.StakingAmountRequirement
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.tokens.model.Amount
|
||||
import com.tangem.features.staking.impl.R
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class AmountRequirementStateTransformerTest {
|
||||
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus = mockk(relaxed = true)
|
||||
|
||||
private fun amountState(enteredCrypto: BigDecimal): AmountState.Data = AmountState.Data(
|
||||
isPrimaryButtonEnabled = true,
|
||||
accountTitleUM = mockk(relaxed = true),
|
||||
availableBalanceCrypto = mockk(relaxed = true),
|
||||
availableBalanceFiat = mockk(relaxed = true),
|
||||
tokenName = mockk(relaxed = true),
|
||||
tokenIconState = mockk(relaxed = true),
|
||||
amountTextField = AmountFieldModel(
|
||||
value = enteredCrypto.toPlainString(),
|
||||
onValueChange = {},
|
||||
keyboardOptions = mockk(relaxed = true),
|
||||
keyboardActions = mockk(relaxed = true),
|
||||
cryptoAmount = Amount(currencySymbol = "ETH", value = enteredCrypto, decimals = 18),
|
||||
fiatAmount = Amount(currencySymbol = "USD", value = BigDecimal.ZERO, decimals = 2),
|
||||
isFiatValue = false,
|
||||
fiatValue = "0",
|
||||
isFiatUnavailable = false,
|
||||
isValuePasted = false,
|
||||
onValuePastedTriggerDismiss = {},
|
||||
isError = false,
|
||||
isWarning = false,
|
||||
error = stringReference(""),
|
||||
),
|
||||
appCurrency = mockk(relaxed = true),
|
||||
)
|
||||
|
||||
private fun enterIntegrationWith(minimum: BigDecimal?, maximum: BigDecimal?): StakingIntegration = mockk {
|
||||
every { enterArgs } returns StakingActionArgs(
|
||||
amountRequirement = StakingAmountRequirement(
|
||||
isRequired = true,
|
||||
minimum = minimum,
|
||||
maximum = maximum,
|
||||
),
|
||||
isPartialAmountDisabled = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun exitIntegrationWith(minimum: BigDecimal?, maximum: BigDecimal?): StakingIntegration = mockk {
|
||||
every { exitArgs } returns StakingActionArgs(
|
||||
amountRequirement = StakingAmountRequirement(
|
||||
isRequired = true,
|
||||
minimum = minimum,
|
||||
maximum = maximum,
|
||||
),
|
||||
isPartialAmountDisabled = false,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN amount exceeds positive maximum THEN max amount error string is used`() {
|
||||
val transformer = AmountRequirementStateTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
maxAmount = EnterAmountBoundary(amount = BigDecimal("100"), fiatAmount = null, fiatRate = null),
|
||||
integration = enterIntegrationWith(minimum = BigDecimal("0.01"), maximum = BigDecimal("0.15")),
|
||||
actionType = StakingActionCommonType.Enter(skipEnterAmount = false),
|
||||
)
|
||||
|
||||
val result = transformer.transform(amountState(BigDecimal("0.2"))) as AmountState.Data
|
||||
|
||||
assertThat(result.amountTextField.isError).isTrue()
|
||||
assertThat((result.amountTextField.error as TextReference.Res).id)
|
||||
.isEqualTo(R.string.staking_max_amount_requirement_error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN amount below minimum THEN min amount error string is used`() {
|
||||
val transformer = AmountRequirementStateTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
maxAmount = EnterAmountBoundary(amount = BigDecimal("100"), fiatAmount = null, fiatRate = null),
|
||||
integration = enterIntegrationWith(minimum = BigDecimal("0.1"), maximum = null),
|
||||
actionType = StakingActionCommonType.Enter(skipEnterAmount = false),
|
||||
)
|
||||
|
||||
val result = transformer.transform(amountState(BigDecimal("0.05"))) as AmountState.Data
|
||||
|
||||
assertThat(result.amountTextField.isError).isTrue()
|
||||
assertThat((result.amountTextField.error as TextReference.Res).id)
|
||||
.isEqualTo(R.string.staking_amount_requirement_error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN Exit action and amount below exit minimum THEN unstake min error string is used`() {
|
||||
val transformer = AmountRequirementStateTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
maxAmount = EnterAmountBoundary(amount = BigDecimal("100"), fiatAmount = null, fiatRate = null),
|
||||
integration = exitIntegrationWith(minimum = BigDecimal("0.1"), maximum = null),
|
||||
actionType = StakingActionCommonType.Exit(partiallyUnstakeDisabled = false),
|
||||
)
|
||||
|
||||
val result = transformer.transform(amountState(BigDecimal("0.05"))) as AmountState.Data
|
||||
|
||||
assertThat(result.amountTextField.isError).isTrue()
|
||||
assertThat((result.amountTextField.error as TextReference.Res).id)
|
||||
.isEqualTo(R.string.staking_unstake_amount_requirement_error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN Enter action and maximum is null and amount exceeds balance cap THEN max error string is used`() {
|
||||
val transformer = AmountRequirementStateTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
maxAmount = EnterAmountBoundary(amount = BigDecimal("0.5"), fiatAmount = null, fiatRate = null),
|
||||
integration = enterIntegrationWith(minimum = BigDecimal("0.01"), maximum = null),
|
||||
actionType = StakingActionCommonType.Enter(skipEnterAmount = false),
|
||||
)
|
||||
|
||||
val result = transformer.transform(amountState(BigDecimal("0.6"))) as AmountState.Data
|
||||
|
||||
assertThat(result.amountTextField.isError).isTrue()
|
||||
assertThat((result.amountTextField.error as TextReference.Res).id)
|
||||
.isEqualTo(R.string.staking_max_amount_requirement_error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN Exit action and amount exceeds staked balance THEN max amount error string is used`() {
|
||||
val transformer = AmountRequirementStateTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
maxAmount = EnterAmountBoundary(amount = BigDecimal("0.5"), fiatAmount = null, fiatRate = null),
|
||||
integration = exitIntegrationWith(minimum = BigDecimal("0.01"), maximum = null),
|
||||
actionType = StakingActionCommonType.Exit(partiallyUnstakeDisabled = false),
|
||||
)
|
||||
|
||||
val result = transformer.transform(amountState(BigDecimal("0.6"))) as AmountState.Data
|
||||
|
||||
assertThat(result.amountTextField.isError).isTrue()
|
||||
assertThat((result.amountTextField.error as TextReference.Res).id)
|
||||
.isEqualTo(R.string.staking_max_amount_requirement_error)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue