Updated on 2026-08-14
This commit is contained in:
commit
0470e1f010
1144 changed files with 48463 additions and 11166 deletions
|
|
@ -10,6 +10,10 @@ android {
|
|||
namespace = "com.tangem.features.staking.impl"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** AndroidX */
|
||||
implementation(deps.androidx.fragment.ktx)
|
||||
|
|
@ -88,4 +92,11 @@ dependencies {
|
|||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Test */
|
||||
testImplementation(deps.test.junit5)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(deps.test.coroutine)
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>BooleanPropertyNaming:AddStakingNotificationsTransformer.kt$AddStakingNotificationsTransformer$val showNotification = sendingAmount + feeAmount > balance</ID>
|
||||
<ID>BooleanPropertyNaming:AmountCurrencyChangeStateTransformer.kt$AmountCurrencyChangeStateTransformer$private val value: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:StakingUiState.kt$StakingStates.InitialInfoState.Data$val showBanner: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:StakingUiState.kt$StakingUiState$val showColdWalletInteractionIcon: Boolean</ID>
|
||||
<ID>CastNullableToNonNullableType:SetApprovalBottomSheetInProgressTransformer.kt$SetApprovalBottomSheetInProgressTransformer$as</ID>
|
||||
<ID>CastNullableToNonNullableType:SetApprovalBottomSheetTypeChangeTransformer.kt$SetApprovalBottomSheetTypeChangeTransformer$as</ID>
|
||||
<ID>MultilineLambdaItParameter:AddStakingNotificationsTransformer.kt$AddStakingNotificationsTransformer${ it is StakingNotification.Error || it is NotificationUM.Error || it is NotificationUM.Warning.NetworkFeeUnreachable || it is StakingNotification.Warning.TransactionInProgress || it is StakingNotification.Warning.InitializeTonAccount }</ID>
|
||||
<ID>MultilineLambdaItParameter:DefaultStakingDeepLinkHandler.kt$DefaultStakingDeepLinkHandler${ val isNetwork = it.network.backendId.equals(networkId, ignoreCase = true) val isCurrency = it.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true isNetwork && isCurrency }</ID>
|
||||
<ID>MultilineLambdaItParameter:StakingFeeBlock.kt${ if (it == FeeState.Error) { Text( text = DASH_SIGN, color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.body1, ) } }</ID>
|
||||
<ID>MultilineLambdaItParameter:StakingFeeBlock.kt${ if (it == FeeState.Loading) { RectangleShimmer( radius = TangemTheme.dimens.radius3, modifier = Modifier.size( height = TangemTheme.dimens.size24, width = TangemTheme.dimens.size90, ), ) } }</ID>
|
||||
<ID>MultilineLambdaItParameter:StakingInfoNotificationsFactory.kt$StakingInfoNotificationsFactory${ it.type == BalanceType.PREPARING || it.type == BalanceType.STAKED || it.type == BalanceType.LOCKED }</ID>
|
||||
<ID>MultilineLambdaItParameter:StakingStateController.kt$StakingStateController${ it.copy( showColdWalletInteractionIcon = userWallet is UserWallet.Cold, ) }</ID>
|
||||
<ID>NullableToStringCall:DefaultStakingDeepLinkHandler.kt$DefaultStakingDeepLinkHandler$$networkId</ID>
|
||||
<ID>NullableToStringCall:DefaultStakingDeepLinkHandler.kt$DefaultStakingDeepLinkHandler$$tokenId</ID>
|
||||
<ID>PropertyUsedBeforeDeclaration:StakingFeeBlock.kt$FeeBlockPreviewProvider$contentState</ID>
|
||||
<ID>PropertyUsedBeforeDeclaration:StakingStateController.kt$StakingStateController$uiState</ID>
|
||||
<ID>UnnecessaryEventHandlerParameter:StakingInitialInfoContent.kt$onClick: (BalanceState) -> Unit</ID>
|
||||
<ID>UnnecessaryLet:StakingTosText.kt$let { onTextClick(PRIVACY_POLICY_URL) }</ID>
|
||||
<ID>UnnecessaryLet:StakingTosText.kt$let { onTextClick(TERMS_OF_USE_URL) }</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -53,9 +53,9 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
|
|||
params = MultiWalletCryptoCurrenciesProducer.Params(selectedUserWalletId),
|
||||
)
|
||||
.orEmpty()
|
||||
.firstOrNull {
|
||||
val isNetwork = it.network.backendId.equals(networkId, ignoreCase = true)
|
||||
val isCurrency = it.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true
|
||||
.firstOrNull { currency ->
|
||||
val isNetwork = currency.network.rawId.equals(networkId, ignoreCase = true)
|
||||
val isCurrency = currency.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true
|
||||
isNetwork && isCurrency
|
||||
}
|
||||
|
||||
|
|
@ -63,8 +63,8 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
|
|||
TangemLogger.e(
|
||||
"""
|
||||
Could not get crypto currency for
|
||||
|- $NETWORK_ID_KEY: $networkId
|
||||
|- $TOKEN_ID_KEY: $tokenId
|
||||
|- $NETWORK_ID_KEY: ${networkId.orEmpty()}
|
||||
|- $TOKEN_ID_KEY: ${tokenId.orEmpty()}
|
||||
""".trimIndent(),
|
||||
)
|
||||
return@launch
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.staking.impl.presentation.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
|
|
@ -11,6 +12,7 @@ import java.math.BigDecimal
|
|||
|
||||
// TODO split this interface to click intents and other interaction events
|
||||
@Suppress("TooManyFunctions")
|
||||
@Immutable
|
||||
internal interface StakingClickIntents : AmountScreenClickIntents {
|
||||
|
||||
fun onBackClick()
|
||||
|
|
|
|||
|
|
@ -2,21 +2,20 @@ package com.tangem.features.staking.impl.presentation.model
|
|||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.TangemBlogUrlBuilder
|
||||
import com.tangem.common.getValidatorsCount
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer.ReduceByData
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig
|
||||
import com.tangem.features.approval.api.GiveApprovalComponent
|
||||
import com.tangem.features.approval.api.GiveApprovalFeatureToggles
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.api.ParamsInterceptorHolder
|
||||
|
|
@ -36,11 +35,11 @@ import com.tangem.core.ui.haptic.TangemHapticEffect
|
|||
import com.tangem.core.ui.haptic.VibratorHapticManager
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||
import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase
|
||||
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
||||
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
|
|
@ -61,12 +60,13 @@ import com.tangem.domain.staking.model.stakekit.action.StakingAction
|
|||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
|
||||
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
|
||||
import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase
|
||||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.usecase.*
|
||||
import com.tangem.domain.utils.convertToSdkAmount
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.approval.api.GiveApprovalComponent
|
||||
import com.tangem.features.approval.api.GiveApprovalFeatureToggles
|
||||
import com.tangem.features.staking.api.StakingComponent
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.analytics.StakingParamsInterceptor
|
||||
|
|
@ -96,21 +96,19 @@ import com.tangem.features.staking.impl.presentation.state.utils.isSingleAction
|
|||
import com.tangem.features.staking.impl.presentation.state.utils.withStubUnstakeAction
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isTon
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP
|
||||
import com.tangem.utils.coroutines.*
|
||||
import com.tangem.utils.extensions.isSingleItem
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import java.math.BigDecimal
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import javax.inject.Inject
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
@Suppress("LargeClass", "TooManyFunctions", "LongParameterList")
|
||||
@Stable
|
||||
|
|
@ -122,7 +120,7 @@ internal class StakingModel @Inject constructor(
|
|||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
|
||||
private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val sendTransactionUseCase: SendTransactionUseCase,
|
||||
private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase,
|
||||
|
|
@ -205,7 +203,13 @@ internal class StakingModel @Inject constructor(
|
|||
getUserWalletUseCase(userWalletId).getOrNull(),
|
||||
) { "No wallet found for id: $userWalletId" }
|
||||
}
|
||||
private var appCurrency: AppCurrency by Delegates.notNull()
|
||||
private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
}.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = AppCurrency.Default,
|
||||
)
|
||||
|
||||
private val balancesToShow: List<StakingBalanceEntry>
|
||||
get() {
|
||||
|
|
@ -310,7 +314,6 @@ internal class StakingModel @Inject constructor(
|
|||
private val balanceHidingJobHolder = JobHolder()
|
||||
|
||||
init {
|
||||
subscribeOnSelectedAppCurrency()
|
||||
subscribeOnCurrencyStatusUpdates()
|
||||
stateController.initializeWithUserWallet(userWallet)
|
||||
}
|
||||
|
|
@ -364,7 +367,7 @@ internal class StakingModel @Inject constructor(
|
|||
clickIntents = this@StakingModel,
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
userWalletProvider = Provider { userWallet },
|
||||
appCurrencyProvider = Provider { appCurrency },
|
||||
appCurrencyProvider = Provider { currentAppCurrency.value },
|
||||
isBalanceHidden = isBalanceHiddenFlow.value,
|
||||
isAccountsModeEnabled = isAccountsModeEnabled,
|
||||
account = account,
|
||||
|
|
@ -398,7 +401,7 @@ internal class StakingModel @Inject constructor(
|
|||
stateController.update(
|
||||
SetConfirmationStateLoadingTransformer(
|
||||
integration = integration,
|
||||
appCurrency = appCurrency,
|
||||
appCurrency = currentAppCurrency.value,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
),
|
||||
)
|
||||
|
|
@ -407,7 +410,7 @@ internal class StakingModel @Inject constructor(
|
|||
onStakingFee = { gasEstimate, isFeeApproximate ->
|
||||
stateController.update(
|
||||
SetConfirmationStateAssentTransformer(
|
||||
appCurrencyProvider = Provider { appCurrency },
|
||||
appCurrencyProvider = Provider { currentAppCurrency.value },
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
fee = gasEstimate,
|
||||
isFeeApproximate = isFeeApproximate,
|
||||
|
|
@ -432,7 +435,7 @@ internal class StakingModel @Inject constructor(
|
|||
onApprovalFee = { fee ->
|
||||
stateController.update(
|
||||
SetConfirmationStateAssentApprovalTransformer(
|
||||
appCurrencyProvider = Provider { appCurrency },
|
||||
appCurrencyProvider = Provider { currentAppCurrency.value },
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
fee = fee,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
|
|
@ -450,10 +453,9 @@ internal class StakingModel @Inject constructor(
|
|||
stakingAnalyticSender.sendTransactionStakingClickedAnalytics(value)
|
||||
stateController.update(SetConfirmationStateInProgressTransformer())
|
||||
|
||||
if (integration is P2PEthPoolIntegration) {
|
||||
checkFeeAndSendP2PTransaction()
|
||||
} else {
|
||||
sendTransaction()
|
||||
when (integration) {
|
||||
is P2PEthPoolIntegration -> checkFeeAndSendP2PTransaction()
|
||||
is StakeKitIntegration -> sendTransaction()
|
||||
}
|
||||
}.saveIn(sendTransactionJobHolder)
|
||||
}
|
||||
|
|
@ -543,7 +545,7 @@ internal class StakingModel @Inject constructor(
|
|||
stateController.updateAll(
|
||||
SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus),
|
||||
SetConfirmationStateAssentTransformer(
|
||||
appCurrencyProvider = Provider { appCurrency },
|
||||
appCurrencyProvider = Provider { currentAppCurrency.value },
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
fee = increasedFee,
|
||||
isFeeApproximate = isFeeApproximate,
|
||||
|
|
@ -601,7 +603,9 @@ internal class StakingModel @Inject constructor(
|
|||
|
||||
override fun onInitialInfoBannerClick() {
|
||||
analyticsEventHandler.send(StakingAnalyticsEvent.WhatIsStaking())
|
||||
innerRouter.openUrl(WHAT_IS_STAKING_ARTICLE_URL)
|
||||
modelScope.launch {
|
||||
innerRouter.openUrl(TangemBlogUrlBuilder.build(TangemBlogUrlBuilder.Post.HowToStake))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInfoClick(infoType: InfoType) {
|
||||
|
|
@ -791,7 +795,7 @@ internal class StakingModel @Inject constructor(
|
|||
stateController.update(
|
||||
ShowApprovalBottomSheetTransformer(
|
||||
userWallet = userWallet,
|
||||
appCurrencyProvider = Provider { appCurrency },
|
||||
appCurrencyProvider = Provider { currentAppCurrency.value },
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
) {
|
||||
|
|
@ -843,7 +847,7 @@ internal class StakingModel @Inject constructor(
|
|||
)
|
||||
stateController.update(
|
||||
SetConfirmationStateAssentApprovalTransformer(
|
||||
appCurrencyProvider = Provider { appCurrency },
|
||||
appCurrencyProvider = Provider { currentAppCurrency.value },
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
fee = TransactionFee.Single(fee),
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
|
|
@ -872,7 +876,7 @@ internal class StakingModel @Inject constructor(
|
|||
)
|
||||
stateController.update(
|
||||
SetConfirmationStateAssentApprovalTransformer(
|
||||
appCurrencyProvider = Provider { appCurrency },
|
||||
appCurrencyProvider = Provider { currentAppCurrency.value },
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
fee = TransactionFee.Single(fee),
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
|
|
@ -930,7 +934,7 @@ internal class StakingModel @Inject constructor(
|
|||
stateController.update(
|
||||
AddStakingNotificationsTransformer(
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
appCurrencyProvider = Provider { appCurrency },
|
||||
appCurrencyProvider = Provider { currentAppCurrency.value },
|
||||
isAccountInitializedProvider = Provider { isAccountInitialized },
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
currencyWarning = currencyWarning,
|
||||
|
|
@ -949,7 +953,7 @@ internal class StakingModel @Inject constructor(
|
|||
reduceAmountByDiff: BigDecimal,
|
||||
notification: Class<out NotificationUM>,
|
||||
) {
|
||||
AmountReduceByStateTransformer(
|
||||
val transformer = AmountReduceByStateTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
minimumTransactionAmount = minimumTransactionAmount,
|
||||
value = ReduceByData(
|
||||
|
|
@ -957,6 +961,7 @@ internal class StakingModel @Inject constructor(
|
|||
reduceAmountByDiff = reduceAmountByDiff,
|
||||
),
|
||||
)
|
||||
stateController.update(transformer)
|
||||
onNotificationCancel(notification)
|
||||
}
|
||||
|
||||
|
|
@ -1054,8 +1059,9 @@ internal class StakingModel @Inject constructor(
|
|||
modelScope.launch {
|
||||
val network = cryptoCurrencyStatus.currency.network
|
||||
|
||||
val metaInfo =
|
||||
getWalletMetaInfoUseCase(userWallet.walletId).getOrElse { error("CardInfo must be not null") }
|
||||
val metaInfo = getWalletMetaInfoUseCase(userWalletId = userWallet.walletId).getOrElse {
|
||||
error("CardInfo must be not null")
|
||||
}
|
||||
val amountState = uiState.value.amountState as? AmountState.Data
|
||||
val confirmationState = uiState.value.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
val validatorState = uiState.value.validatorState as? StakingStates.ValidatorState.Data
|
||||
|
|
@ -1067,8 +1073,7 @@ internal class StakingModel @Inject constructor(
|
|||
saveBlockchainErrorUseCase(
|
||||
error = BlockchainErrorInfo(
|
||||
errorMessage = errorMessage,
|
||||
blockchainId = network.rawId,
|
||||
derivationPath = network.derivationPath.value,
|
||||
networkId = network.id,
|
||||
destinationAddress = target?.address.orEmpty(),
|
||||
tokenSymbol = (cryptoCurrencyStatus.currency as? CryptoCurrency.Token)?.symbol,
|
||||
amount = amount?.run { value?.toPlainString() + currencySymbol }.orEmpty(),
|
||||
|
|
@ -1097,7 +1102,9 @@ internal class StakingModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onOpenLearnMoreAboutApproveClick() {
|
||||
urlOpener.openUrl(RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP)
|
||||
modelScope.launch {
|
||||
urlOpener.openUrl(TangemBlogUrlBuilder.build(TangemBlogUrlBuilder.Post.GiveRevokePermission))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivateTonAccountNotificationClick() {
|
||||
|
|
@ -1151,7 +1158,7 @@ internal class StakingModel @Inject constructor(
|
|||
ifRight = { fee ->
|
||||
stateController.update(
|
||||
SetFeeToTonInitializeBottomSheetTransformer(
|
||||
appCurrencyProvider = Provider { appCurrency },
|
||||
appCurrencyProvider = Provider { currentAppCurrency.value },
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
fee = fee.normal,
|
||||
isFeeApproximate = false,
|
||||
|
|
@ -1305,7 +1312,7 @@ internal class StakingModel @Inject constructor(
|
|||
transformer = HideBalanceStateTransformer(
|
||||
isBalanceHidden = settings.isBalanceHidden,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
appCurrency = appCurrency,
|
||||
appCurrency = currentAppCurrency.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -1314,17 +1321,6 @@ internal class StakingModel @Inject constructor(
|
|||
.saveIn(balanceHidingJobHolder)
|
||||
}
|
||||
|
||||
private fun subscribeOnSelectedAppCurrency() {
|
||||
getSelectedAppCurrencyUseCase()
|
||||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.onEach { maybeAppCurrency ->
|
||||
appCurrency = maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
}
|
||||
.flowOn(dispatchers.main)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeOnStepChanges(status: CryptoCurrencyStatus) {
|
||||
uiState
|
||||
.distinctUntilChangedBy { it.currentStep }
|
||||
|
|
@ -1381,7 +1377,7 @@ internal class StakingModel @Inject constructor(
|
|||
isAnyTokenStaked = isAnyTokenStaked,
|
||||
cryptoCurrencyStatus = status,
|
||||
userWalletProvider = Provider { userWallet },
|
||||
appCurrencyProvider = Provider { appCurrency },
|
||||
appCurrencyProvider = Provider { currentAppCurrency.value },
|
||||
balancesToShowProvider = Provider { balancesToShow },
|
||||
isAccountsModeEnabled = isAccountsModeEnabled,
|
||||
account = account,
|
||||
|
|
@ -1419,7 +1415,7 @@ internal class StakingModel @Inject constructor(
|
|||
clickIntents = this,
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
userWalletProvider = Provider { userWallet },
|
||||
appCurrencyProvider = Provider { appCurrency },
|
||||
appCurrencyProvider = Provider { currentAppCurrency.value },
|
||||
isAccountsModeEnabled = isAccountsModeEnabled,
|
||||
isBalanceHidden = isBalanceHiddenFlow.value,
|
||||
account = account,
|
||||
|
|
@ -1503,7 +1499,6 @@ internal class StakingModel @Inject constructor(
|
|||
}
|
||||
|
||||
private companion object {
|
||||
const val WHAT_IS_STAKING_ARTICLE_URL = "https://tangem.com/en/blog/post/how-to-stake-cryptocurrency/"
|
||||
const val ALLOWANCE_UPDATE_DELAY = 10_000L
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,6 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles
|
||||
import com.tangem.domain.models.wallet.isColdWallet
|
||||
import com.tangem.domain.models.wallet.isHotWallet
|
||||
import javax.inject.Inject
|
||||
|
|
@ -23,24 +22,22 @@ import javax.inject.Inject
|
|||
@ModelScoped
|
||||
internal class StakingStateController @Inject constructor(
|
||||
urlOpener: UrlOpener,
|
||||
private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles,
|
||||
) {
|
||||
|
||||
val value: StakingUiState get() = uiState.value
|
||||
|
||||
private val mutableUiState: MutableStateFlow<StakingUiState> = MutableStateFlow(value = getInitialState())
|
||||
|
||||
val uiState: StateFlow<StakingUiState> get() = mutableUiState.asStateFlow()
|
||||
|
||||
val value: StakingUiState get() = uiState.value
|
||||
|
||||
private val buttonsTransformer = SetButtonsStateTransformer(urlOpener)
|
||||
private val titleTransformer = SetTitleTransformer
|
||||
|
||||
fun initializeWithUserWallet(userWallet: UserWallet) {
|
||||
mutableUiState.update { state ->
|
||||
state.copy(
|
||||
showColdWalletInteractionIcon = userWallet.isColdWallet,
|
||||
shouldShowHoldToConfirmButton = holdToConfirmButtonFeatureToggles.isHoldToConfirmEnabled &&
|
||||
userWallet.isHotWallet,
|
||||
isColdWalletInteractionIconVisible = userWallet.isColdWallet,
|
||||
shouldShowHoldToConfirmButton = userWallet.isHotWallet,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -89,7 +86,7 @@ internal class StakingStateController @Inject constructor(
|
|||
actionType = StakingActionCommonType.Enter(skipEnterAmount = false),
|
||||
buttonsState = NavigationButtonsState.Empty,
|
||||
balanceState = null,
|
||||
showColdWalletInteractionIcon = true,
|
||||
isColdWalletInteractionIconVisible = true,
|
||||
shouldShowHoldToConfirmButton = false,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,22 +39,9 @@ internal data class StakingUiState(
|
|||
val actionType: StakingActionCommonType,
|
||||
val buttonsState: NavigationButtonsState,
|
||||
val balanceState: BalanceState?,
|
||||
val showColdWalletInteractionIcon: Boolean,
|
||||
val isColdWalletInteractionIconVisible: Boolean,
|
||||
val shouldShowHoldToConfirmButton: Boolean,
|
||||
) {
|
||||
|
||||
fun copyWrapped(
|
||||
initialInfoState: StakingStates.InitialInfoState = this.initialInfoState,
|
||||
amountState: AmountState = this.amountState,
|
||||
confirmationState: StakingStates.ConfirmationState = this.confirmationState,
|
||||
validatorState: StakingStates.ValidatorState = this.validatorState,
|
||||
): StakingUiState = copy(
|
||||
initialInfoState = initialInfoState,
|
||||
amountState = amountState,
|
||||
confirmationState = confirmationState,
|
||||
validatorState = validatorState,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
internal sealed class StakingStates {
|
||||
|
||||
|
|
@ -64,7 +51,7 @@ internal sealed class StakingStates {
|
|||
sealed class InitialInfoState : StakingStates() {
|
||||
data class Data(
|
||||
override val isPrimaryButtonEnabled: Boolean,
|
||||
val showBanner: Boolean,
|
||||
val isBannerVisible: Boolean,
|
||||
val infoItems: ImmutableList<RoundedListWithDividersItemData>,
|
||||
val onInfoClick: (InfoType) -> Unit,
|
||||
val yieldBalance: InnerYieldBalanceState,
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ internal class StakingBalanceEntryConverter(
|
|||
|
||||
private fun StakingBalanceEntry.getBalanceValue(): BigDecimal {
|
||||
val isIncludeStakingTotalBalance = BlockchainUtils.isIncludeStakingTotalBalance(
|
||||
blockchainId = cryptoCurrencyStatus.currency.network.rawId,
|
||||
networkId = cryptoCurrencyStatus.currency.network.rawId,
|
||||
)
|
||||
return if (isIncludeStakingTotalBalance) {
|
||||
amount
|
||||
|
|
|
|||
|
|
@ -33,13 +33,13 @@ import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
|||
import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount
|
||||
import com.tangem.features.staking.impl.presentation.state.utils.isCompositePendingActions
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -165,7 +165,7 @@ internal class StakeKitTransactionSender @AssistedInject constructor(
|
|||
?.map { transaction ->
|
||||
async {
|
||||
getConstructedStakingTransactionUseCase(
|
||||
networkId = cryptoCurrencyStatus.currency.network.rawId,
|
||||
networkId = cryptoCurrencyStatus.currency.network.id.rawId,
|
||||
fee = fee,
|
||||
amount = amount.convertToSdkAmount(cryptoCurrencyStatus),
|
||||
transactionId = transaction.id,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
internal object InitialStakingStatePreview {
|
||||
val defaultState = StakingStates.InitialInfoState.Data(
|
||||
isPrimaryButtonEnabled = true,
|
||||
showBanner = true,
|
||||
isBannerVisible = true,
|
||||
infoItems = persistentListOf(
|
||||
RoundedListWithDividersItemData(
|
||||
id = R.string.staking_details_available,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import com.tangem.common.ui.amountScreen.converters.AmountAccountConverter
|
|||
import com.tangem.common.ui.amountScreen.converters.AmountStateConverter
|
||||
import com.tangem.common.ui.amountScreen.models.AmountParameters
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ internal class SetButtonsStateTransformer(
|
|||
return prevState.copy(buttonsState = buttonsState)
|
||||
}
|
||||
|
||||
private fun getPrimaryButton(prevState: StakingUiState): NavigationButton? {
|
||||
private fun getPrimaryButton(prevState: StakingUiState): NavigationButton {
|
||||
val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
val innerConfirmState = confirmState?.innerState
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ internal class SetButtonsStateTransformer(
|
|||
val isPrimaryButtonDisabled = prevState.isPrimaryButtonDisabled()
|
||||
return NavigationButton(
|
||||
textReference = prevState.getButtonText(),
|
||||
iconRes = R.drawable.ic_tangem_24.takeIf { prevState.showColdWalletInteractionIcon },
|
||||
iconRes = R.drawable.ic_tangem_24.takeIf { prevState.isColdWalletInteractionIconVisible },
|
||||
isDimmed = isPrimaryButtonDisabled,
|
||||
isIconVisible = isIconVisible,
|
||||
shouldShowProgress = isInProgress,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ 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.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
|
||||
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.components.list.RoundedListWithDividersItemData
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
|
|
@ -96,7 +96,7 @@ internal class SetInitialDataStateTransformer(
|
|||
isPrimaryButtonEnabled = with(status) {
|
||||
!amount.isNullOrZero() && sources.stakingBalanceSource.isActual() && sources.networkSource.isActual()
|
||||
},
|
||||
showBanner = !isAnyTokenStaked && yieldBalance == InnerYieldBalanceState.Empty,
|
||||
isBannerVisible = !isAnyTokenStaked && yieldBalance == InnerYieldBalanceState.Empty,
|
||||
infoItems = getInfoItems(),
|
||||
onInfoClick = clickIntents::onInfoClick,
|
||||
yieldBalance = yieldBalance,
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ import com.tangem.utils.transformer.Transformer
|
|||
|
||||
internal class AmountCurrencyChangeStateTransformer(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val value: Boolean,
|
||||
private val isFiatValue: Boolean,
|
||||
) : Transformer<StakingUiState> {
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
return prevState.copy(
|
||||
amountState = AmountCurrencyTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState),
|
||||
amountState = AmountCurrencyTransformer(cryptoCurrencyStatus, isFiatValue).transform(prevState.amountState),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ internal class SetApprovalBottomSheetInProgressTransformer(
|
|||
),
|
||||
onCancel = onDismiss,
|
||||
)
|
||||
} as TangemBottomSheetConfigContent,
|
||||
} as? TangemBottomSheetConfigContent ?: return prevState,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ internal class SetApprovalBottomSheetTypeChangeTransformer(
|
|||
bottomSheetConfig = prevState.bottomSheetConfig?.copy(
|
||||
content = approvalBottomSheetConfig?.copy(
|
||||
data = approvalBottomSheetConfig.data.copy(approveType = approveType),
|
||||
) as TangemBottomSheetConfigContent,
|
||||
) as? TangemBottomSheetConfigContent ?: return prevState,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import com.tangem.common.ui.notifications.NotificationsFactory.addFeeUnreachable
|
|||
import com.tangem.common.ui.notifications.NotificationsFactory.addRentExemptionNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountErrorNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification
|
||||
import com.tangem.core.ui.extensions.networkIconResId
|
||||
import com.tangem.common.ui.extensions.networkIconResId
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
|
@ -177,12 +177,12 @@ internal class AddStakingNotificationsTransformer(
|
|||
}
|
||||
|
||||
private fun isPrimaryButtonEnabled(notifications: ImmutableList<NotificationUM>, isActualSources: Boolean) =
|
||||
notifications.none {
|
||||
it is StakingNotification.Error ||
|
||||
it is NotificationUM.Error ||
|
||||
it is NotificationUM.Warning.NetworkFeeUnreachable ||
|
||||
it is StakingNotification.Warning.TransactionInProgress ||
|
||||
it is StakingNotification.Warning.InitializeTonAccount
|
||||
notifications.none { notification ->
|
||||
notification is StakingNotification.Error ||
|
||||
notification is NotificationUM.Error ||
|
||||
notification is NotificationUM.Warning.NetworkFeeUnreachable ||
|
||||
notification is StakingNotification.Warning.TransactionInProgress ||
|
||||
notification is StakingNotification.Warning.InitializeTonAccount
|
||||
} && isActualSources
|
||||
|
||||
private fun MutableList<NotificationUM>.addStakingErrorNotifications(
|
||||
|
|
@ -229,7 +229,7 @@ internal class AddStakingNotificationsTransformer(
|
|||
addExceedsBalanceNotification(
|
||||
cryptoCurrencyWarning = currencyWarning,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
shouldMergeFeeNetworkName = BlockchainUtils.isArbitrum(network.backendId),
|
||||
shouldMergeFeeNetworkName = BlockchainUtils.isArbitrum(network.rawId),
|
||||
onClick = prevState.clickIntents::openTokenDetails,
|
||||
onAnalyticsEvent = { prevState.clickIntents.onNotEnoughFeeNotificationShow() },
|
||||
onResetAnalyticsEvent = { /*no-op*/ },
|
||||
|
|
@ -302,8 +302,8 @@ internal class AddStakingNotificationsTransformer(
|
|||
val balance = cryptoCurrencyStatus.value.amount.orZero()
|
||||
if (!isSubtractionAvailable) return
|
||||
|
||||
val showNotification = sendingAmount + feeAmount > balance
|
||||
if (showNotification) {
|
||||
val isExceedsBalance = sendingAmount + feeAmount > balance
|
||||
if (isExceedsBalance) {
|
||||
onNotEnoughFeeNotificationShow()
|
||||
val notification = if (actionType is StakingActionCommonType.Enter) {
|
||||
NotificationUM.Error.TotalExceedsBalance
|
||||
|
|
@ -315,7 +315,7 @@ internal class AddStakingNotificationsTransformer(
|
|||
currencyName = name,
|
||||
feeName = name,
|
||||
feeSymbol = symbol,
|
||||
mergeFeeNetworkName = BlockchainUtils.isArbitrum(network.backendId),
|
||||
mergeFeeNetworkName = BlockchainUtils.isArbitrum(network.rawId),
|
||||
onClick = { onClick(this) },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,10 +150,10 @@ internal class StakingInfoNotificationsFactory(
|
|||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val isTron = isTron(cryptoCurrencyStatus.currency.network.rawId)
|
||||
val hasStakedBalance = (cryptoCurrencyStatus.value.stakingBalance as? StakingBalance.Data.StakeKit)?.balance
|
||||
?.items?.any {
|
||||
it.type == BalanceType.PREPARING ||
|
||||
it.type == BalanceType.STAKED ||
|
||||
it.type == BalanceType.LOCKED
|
||||
?.items?.any { item ->
|
||||
item.type == BalanceType.PREPARING ||
|
||||
item.type == BalanceType.STAKED ||
|
||||
item.type == BalanceType.LOCKED
|
||||
} == true
|
||||
if (isTron && hasStakedBalance) {
|
||||
add(
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ internal fun StakingInitialInfoContent(
|
|||
.background(TangemTheme.colors.background.secondary)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
if (state.showBanner) {
|
||||
if (state.isBannerVisible) {
|
||||
item(key = BANNER_BLOCK_KEY) {
|
||||
Column(
|
||||
modifier = Modifier.animateItem(),
|
||||
|
|
@ -175,7 +175,7 @@ private fun LazyListScope.activeStakingBlock(
|
|||
ActiveStakingBlock(
|
||||
balance = balance,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
onClick = clickIntents::onActiveStake,
|
||||
onClick = { clickIntents.onActiveStake(balance) },
|
||||
onAnalytic = clickIntents::onActiveStakeAnalytic,
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
|
|
@ -288,7 +288,7 @@ private fun StakingRewardBlock(
|
|||
private fun ActiveStakingBlock(
|
||||
balance: BalanceState,
|
||||
isBalanceHidden: Boolean,
|
||||
onClick: (BalanceState) -> Unit,
|
||||
onClick: () -> Unit,
|
||||
onAnalytic: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -304,7 +304,7 @@ private fun ActiveStakingBlock(
|
|||
enabled = balance.isClickable,
|
||||
onClick = {
|
||||
onAnalytic()
|
||||
onClick(balance)
|
||||
onClick()
|
||||
},
|
||||
)
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
|
|
@ -351,20 +351,18 @@ private fun ActiveStakingBlock(
|
|||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
if (balance.formattedCryptoAmount != null) {
|
||||
Text(
|
||||
text = balance.formattedCryptoAmount.orMaskWithStars(isBalanceHidden).resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing2),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = balance.formattedCryptoAmount.orMaskWithStars(isBalanceHidden).resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing2),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.StakingBalanceIcon(balance: BalanceState, icon: Int?, iconTint: Color) {
|
||||
private fun StakingBalanceIcon(balance: BalanceState, icon: Int?, iconTint: Color) {
|
||||
if (balance.hasImage() || icon != null) {
|
||||
StakingTargetIcon(
|
||||
image = if (balance.hasImage()) balance.target?.image.toImageReference() else null,
|
||||
|
|
|
|||
|
|
@ -109,8 +109,8 @@ private fun BoxScope.FeeLoading(feeState: FeeState) {
|
|||
targetState = feeState,
|
||||
label = "Fee Loading State Change",
|
||||
modifier = Modifier.align(Alignment.CenterEnd),
|
||||
) {
|
||||
if (it == FeeState.Loading) {
|
||||
) { state ->
|
||||
if (state == FeeState.Loading) {
|
||||
RectangleShimmer(
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
modifier = Modifier.size(
|
||||
|
|
@ -128,8 +128,8 @@ private fun BoxScope.FeeError(feeState: FeeState) {
|
|||
targetState = feeState,
|
||||
label = "Fee Error State Change",
|
||||
modifier = Modifier.align(Alignment.CenterEnd),
|
||||
) {
|
||||
if (it == FeeState.Error) {
|
||||
) { state ->
|
||||
if (state == FeeState.Error) {
|
||||
Text(
|
||||
text = DASH_SIGN,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
|
|
@ -151,13 +151,6 @@ private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) va
|
|||
|
||||
private class FeeBlockPreviewProvider : PreviewParameterProvider<FeeState> {
|
||||
|
||||
override val values: Sequence<FeeState>
|
||||
get() = sequenceOf(
|
||||
contentState,
|
||||
FeeState.Loading,
|
||||
FeeState.Error,
|
||||
)
|
||||
|
||||
private val fee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "MATIC",
|
||||
|
|
@ -174,6 +167,13 @@ private class FeeBlockPreviewProvider : PreviewParameterProvider<FeeState> {
|
|||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = true,
|
||||
)
|
||||
|
||||
override val values: Sequence<FeeState>
|
||||
get() = sequenceOf(
|
||||
contentState,
|
||||
FeeState.Loading,
|
||||
FeeState.Error,
|
||||
)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
package com.tangem.features.staking.impl.presentation.model
|
||||
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.amount.*
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.notifications.DismissStakingNotificationsStateTransformer
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class StakingModelAmountTest : StakingModelTestBase() {
|
||||
|
||||
@Test
|
||||
fun `WHEN onAmountPasteTriggerDismiss THEN AmountPasteDismissStateTransformer applied`() = runTest {
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onAmountPasteTriggerDismiss()
|
||||
|
||||
verify {
|
||||
stateController.update(
|
||||
transformer = match<Transformer<StakingUiState>> { it is AmountPasteDismissStateTransformer }
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN onMaxValueClick THEN analytics sent and AmountMaxValueStateTransformer applied`() = runTest {
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onMaxValueClick()
|
||||
|
||||
verify {
|
||||
analyticsEventHandler.send(
|
||||
match {
|
||||
it is StakingAnalyticsEvent.ButtonMax
|
||||
}
|
||||
)
|
||||
}
|
||||
verify {
|
||||
stateController.update(
|
||||
match<Transformer<StakingUiState>> { it is AmountMaxValueStateTransformer }
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN onCurrencyChangeClick THEN analytics sent and AmountCurrencyChangeStateTransformer applied`() = runTest {
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onCurrencyChangeClick(isFiat = true)
|
||||
|
||||
verify {
|
||||
analyticsEventHandler.send(match { it is StakingAnalyticsEvent.AmountSelectCurrency })
|
||||
}
|
||||
verify {
|
||||
stateController.update(
|
||||
transformer = match<Transformer<StakingUiState>> { it is AmountCurrencyChangeStateTransformer }
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN onAmountReduceByClick THEN AmountReduceByStateTransformer and DismissNotification applied`() = runTest {
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onAmountReduceByClick(
|
||||
reduceAmountBy = BigDecimal.ONE,
|
||||
reduceAmountByDiff = BigDecimal.TEN,
|
||||
notification = NotificationUM::class.java,
|
||||
)
|
||||
|
||||
verify {
|
||||
stateController.update(
|
||||
transformer = match<Transformer<StakingUiState>> { it is AmountReduceByStateTransformer }
|
||||
)
|
||||
}
|
||||
verify {
|
||||
stateController.update(
|
||||
transformer = match<Transformer<StakingUiState>> { it is DismissStakingNotificationsStateTransformer }
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN onAmountReduceToClick THEN AmountReduceToStateTransformer and DismissNotification applied`() = runTest {
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onAmountReduceToClick(
|
||||
reduceAmountTo = BigDecimal.ONE,
|
||||
notification = NotificationUM::class.java,
|
||||
)
|
||||
|
||||
verify {
|
||||
stateController.update(
|
||||
transformer = match<Transformer<StakingUiState>> { it is AmountReduceToStateTransformer }
|
||||
)
|
||||
}
|
||||
verify {
|
||||
stateController.update(
|
||||
transformer = match<Transformer<StakingUiState>> {
|
||||
it is DismissStakingNotificationsStateTransformer
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN onAmountReduceByFeeClick THEN AmountReduceByStateTransformer and DismissNotification applied`() =
|
||||
runTest {
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onAmountReduceByFeeClick(
|
||||
reduceAmount = BigDecimal.ONE,
|
||||
notification = NotificationUM::class.java,
|
||||
)
|
||||
|
||||
verify {
|
||||
stateController.update(
|
||||
transformer = match<Transformer<StakingUiState>> { it is AmountReduceByStateTransformer }
|
||||
)
|
||||
}
|
||||
verify {
|
||||
stateController.update(
|
||||
transformer = match<Transformer<StakingUiState>> {
|
||||
it is DismissStakingNotificationsStateTransformer
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN onNotificationCancel THEN DismissStakingNotificationsStateTransformer applied`() = runTest {
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onNotificationCancel(NotificationUM::class.java)
|
||||
|
||||
verify {
|
||||
stateController.update(
|
||||
transformer = match<Transformer<StakingUiState>> { it is DismissStakingNotificationsStateTransformer }
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,327 @@
|
|||
package com.tangem.features.staking.impl.presentation.model
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.domain.balancehiding.BalanceHidingSettings
|
||||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.models.staking.YieldBalanceItem
|
||||
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent
|
||||
import com.tangem.domain.staking.model.StakingApproval
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingAction
|
||||
import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
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.StakingFeeLoader
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.HideBalanceStateTransformer
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.SetConfirmationStateLoadingTransformer
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.SetInitialDataStateTransformer
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class StakingModelInitTest : StakingModelTestBase() {
|
||||
|
||||
@Test
|
||||
fun `GIVEN currency status emitted WHEN model created THEN analytics sent and fee status fetched`() = runTest {
|
||||
val (testCryptoCurrencyStatus, testAccountCurrencyStatus) = createMockedAccountCurrencyStatus()
|
||||
every { testCryptoCurrencyStatus.value } returns mockk {
|
||||
every { stakingBalance } returns mockk<StakingBalance.Data.StakeKit> {
|
||||
every { balance } returns YieldBalanceItem(
|
||||
items = listOf(
|
||||
mockk { every { validatorAddress } returns "address1" },
|
||||
mockk { every { validatorAddress } returns "address2" },
|
||||
),
|
||||
integrationId = "test"
|
||||
)
|
||||
}
|
||||
}
|
||||
every {
|
||||
getAccountCurrencyStatusUseCase(testUserWalletId, testCryptoCurrency)
|
||||
} returns flowOf(testAccountCurrencyStatus)
|
||||
coEvery {
|
||||
getFeePaidCryptoCurrencyStatusSyncUseCase(testUserWalletId, testCryptoCurrencyStatus)
|
||||
} returns Either.Right(mockk(relaxed = true))
|
||||
coEvery {
|
||||
getMinimumTransactionAmountSyncUseCase(testUserWalletId, testCryptoCurrencyStatus)
|
||||
} returns Either.Left(mockk())
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
paramsInterceptorHolder.addParamsInterceptor(
|
||||
match { it.id() == "StakingParamsInterceptorId" }
|
||||
)
|
||||
}
|
||||
verify {
|
||||
analyticsEventHandler.send(
|
||||
StakingAnalyticsEvent.StakingInfoScreenOpened(
|
||||
validatorsCount = 2
|
||||
),
|
||||
)
|
||||
}
|
||||
verify {
|
||||
stateController.initializeWithUserWallet(testUserWallet)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN currency status emitted twice WHEN model created THEN analytics sent only once`() = runTest {
|
||||
val statusFlow = MutableSharedFlow<com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus>()
|
||||
val (testCryptoCurrencyStatus, testAccountCurrencyStatus) = createMockedAccountCurrencyStatus()
|
||||
every {
|
||||
getAccountCurrencyStatusUseCase(testUserWalletId, testCryptoCurrency)
|
||||
} returns statusFlow
|
||||
coEvery {
|
||||
getFeePaidCryptoCurrencyStatusSyncUseCase(testUserWalletId, testCryptoCurrencyStatus)
|
||||
} returns Either.Right(mockk(relaxed = true))
|
||||
coEvery {
|
||||
getMinimumTransactionAmountSyncUseCase(testUserWalletId, testCryptoCurrencyStatus)
|
||||
} returns Either.Left(mockk())
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
statusFlow.emit(testAccountCurrencyStatus)
|
||||
advanceUntilIdle()
|
||||
statusFlow.emit(testAccountCurrencyStatus)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify(exactly = 1) {
|
||||
analyticsEventHandler.send(
|
||||
event = StakingAnalyticsEvent.StakingInfoScreenOpened(validatorsCount = 0)
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN account initialized WHEN checkForTonHeatupCase THEN no error logged`() = runTest {
|
||||
mockkObject(TangemLogger)
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify { checkAccountInitializedUseCase(testUserWalletId, any()) }
|
||||
verify(exactly = 0) { TangemLogger.e(any(), any()) }
|
||||
|
||||
model.onDestroy()
|
||||
unmockkObject(TangemLogger)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN checkAccountInitialized fails WHEN checkForTonHeatupCase THEN error logged`() = runTest {
|
||||
val testError = RuntimeException("network error")
|
||||
coEvery {
|
||||
checkAccountInitializedUseCase(testUserWalletId, any())
|
||||
} returns Either.Left(testError)
|
||||
mockkObject(TangemLogger)
|
||||
every { TangemLogger.e(any(), any()) } just Runs
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { TangemLogger.e("Error", testError) }
|
||||
|
||||
model.onDestroy()
|
||||
unmockkObject(TangemLogger)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN approval needed WHEN setupApprovalNeeded THEN getAllowanceUseCase called`() = runTest {
|
||||
val spenderAddress = "0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908"
|
||||
mockkObject(StakingIntegrationID.Companion)
|
||||
every {
|
||||
StakingIntegrationID.create(any())
|
||||
} returns mockk {
|
||||
every { approval } returns StakingApproval.Needed(spenderAddress)
|
||||
}
|
||||
coEvery {
|
||||
getAllowanceUseCase(testUserWalletId, any(), spenderAddress)
|
||||
} returns Either.Right(BigDecimal.TEN)
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify {
|
||||
getAllowanceUseCase(testUserWalletId, any(), spenderAddress)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
unmockkObject(StakingIntegrationID.Companion)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN approval needed AND getAllowance fails WHEN setupApprovalNeeded THEN no crash`() = runTest {
|
||||
val spenderAddress = "0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908"
|
||||
mockkObject(StakingIntegrationID.Companion)
|
||||
every {
|
||||
StakingIntegrationID.create(any())
|
||||
} returns mockk {
|
||||
every { approval } returns StakingApproval.Needed(spenderAddress)
|
||||
}
|
||||
coEvery {
|
||||
getAllowanceUseCase(testUserWalletId, any(), spenderAddress)
|
||||
} returns Either.Left(RuntimeException("allowance error")) // error
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify {
|
||||
getAllowanceUseCase(testUserWalletId, any(), spenderAddress)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
unmockkObject(StakingIntegrationID.Companion)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN any token staked WHEN setupIsAnyTokenStaked THEN use case called with correct wallet id`() = runTest {
|
||||
coEvery { isAnyTokenStakedUseCase(testUserWalletId) } returns Either.Right(true)
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify { isAnyTokenStakedUseCase(testUserWalletId) }
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN subtract available WHEN checkIfSubtractAvailable THEN use case called with correct params`() = runTest {
|
||||
coEvery {
|
||||
isAmountSubtractAvailableUseCase(testUserWalletId, any())
|
||||
} returns Either.Right(true)
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify { isAmountSubtractAvailableUseCase(testUserWalletId, any()) }
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN actions emitted WHEN subscribeOnActionsUpdates AND isInitState THEN updateInitialData`() = runTest {
|
||||
val testActions = listOf(mockk<StakingAction>(relaxed = true))
|
||||
every {
|
||||
getActionsUseCase(testUserWalletId, any())
|
||||
} returns flowOf(Either.Right(testActions))
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify(atLeast = 1) {
|
||||
stateController.updateAll(
|
||||
match { it is SetInitialDataStateTransformer },
|
||||
any(),
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN init step WHEN subscribeOnStepChanges THEN updateInitialData and partialUpdate called`() = runTest {
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify(atLeast = 1) {
|
||||
stateController.updateAll(
|
||||
match { it is SetInitialDataStateTransformer },
|
||||
any(),
|
||||
)
|
||||
}
|
||||
coVerify { mockBalanceUpdater.partialUpdate() }
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN assent step AND isWarning WHEN subscribeOnStepChanges THEN getFee AND amount rounded to integer`() =
|
||||
runTest {
|
||||
val uiStateFlow = MutableStateFlow(initialUiState)
|
||||
every { stateController.uiState } returns uiStateFlow
|
||||
every {
|
||||
stakingOperationsFactory.createFeeLoader(
|
||||
cryptoCurrencyStatus = any(),
|
||||
userWallet = any(),
|
||||
integration = any()
|
||||
)
|
||||
} returns mockk<StakingFeeLoader> {
|
||||
coEvery {
|
||||
getFee(any(), any(), any(), any())
|
||||
} just Runs
|
||||
}
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
val assentUiState = mockk<StakingUiState>(relaxed = true) {
|
||||
every { currentStep } returns StakingStep.Confirmation
|
||||
every { confirmationState } returns mockk<StakingStates.ConfirmationState.Data> {
|
||||
every { innerState } returns InnerConfirmationStakingState.ASSENT
|
||||
}
|
||||
every { amountState } returns mockk<AmountState.Data> {
|
||||
every { amountTextField } returns mockk {
|
||||
every { isWarning } returns true
|
||||
}
|
||||
}
|
||||
}
|
||||
uiStateFlow.value = assentUiState
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
stateController.update(
|
||||
match<Transformer<StakingUiState>> { it is SetConfirmationStateLoadingTransformer }
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN balance hidden WHEN subscribeOnBalanceHiding THEN HideBalanceStateTransformer applied`() = runTest {
|
||||
val balanceHidingSettings = BalanceHidingSettings(
|
||||
isHidingEnabledInSettings = true,
|
||||
isBalanceHidden = true,
|
||||
isBalanceHidingNotificationEnabled = false,
|
||||
)
|
||||
every { getBalanceHidingSettingsUseCase() } returns flowOf(balanceHidingSettings)
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
stateController.update(
|
||||
match<Transformer<StakingUiState>> { it is HideBalanceStateTransformer }
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN onDestroy THEN params interceptor removed`() = runTest {
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onDestroy()
|
||||
|
||||
verify {
|
||||
paramsInterceptorHolder.removeParamsInterceptor("StakingParamsInterceptorId")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,501 @@
|
|||
package com.tangem.features.staking.impl.presentation.model
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.common.TangemBlogUrlBuilder
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.ui.haptic.TangemHapticEffect
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
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.transformers.*
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountMaxValueStateTransformer
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.validator.ValidatorSelectChangeTransformer
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import io.mockk.*
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class StakingModelNavigationTest : StakingModelTestBase() {
|
||||
|
||||
@Test
|
||||
fun `WHEN onBackClick THEN router pop and stateController clear called`() = runTest {
|
||||
every { stateController.value } returns initialUiState
|
||||
every { appRouter.pop(any()) } just Runs
|
||||
every { stateController.clear() } just Runs
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onBackClick()
|
||||
|
||||
verify { appRouter.pop(any()) }
|
||||
verify { stateController.clear() }
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN targets AND no yield balance WHEN onNextClick with balance THEN validators unavailable alert sent`() =
|
||||
runTest {
|
||||
every { messageSender.send(any()) } just Runs
|
||||
every { testYield.allValidatorsFull } returns true
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onNextClick(balanceState = null)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
messageSender.send(
|
||||
match { it is DialogMessage } // dialog from StakingModel.stakingEventFactory
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN partial amount disabled WHEN onNextClick with null balance THEN updateAll called with transformers`() =
|
||||
runTest {
|
||||
every { stateController.value } returns initialUiState
|
||||
every { testYield.args.enter.isPartialAmountDisabled } returns true
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onNextClick(balanceState = null)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
stateController.updateAll(
|
||||
match { it is SetConfirmationStateInitTransformer },
|
||||
match { it is ValidatorSelectChangeTransformer },
|
||||
match { it is SetAmountDataTransformer },
|
||||
match { it is AmountMaxValueStateTransformer },
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN non-initial step WHEN onNextClick THEN only stakingStateRouter onNextClick called`() = runTest {
|
||||
val uiStateFlow = MutableStateFlow(initialUiState)
|
||||
every { stateController.uiState } returns uiStateFlow
|
||||
every { stateController.value } returns initialUiState
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
val amountUiState = mockk<StakingUiState>(relaxed = true) {
|
||||
every { currentStep } returns StakingStep.Amount
|
||||
}
|
||||
uiStateFlow.value = amountUiState
|
||||
every { stateController.value } returns amountUiState
|
||||
advanceUntilIdle()
|
||||
|
||||
clearMocks(stateController, answers = false, recordedCalls = true, verificationMarks = true)
|
||||
|
||||
model.onNextClick(balanceState = null)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
stateController.update(match<(StakingUiState) -> StakingUiState> { true })
|
||||
}
|
||||
verify(exactly = 0) {
|
||||
stateController.updateAll(*anyVararg())
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN assent state AND no approval in progress WHEN onPrevClick THEN prev navigated and assent reset`() =
|
||||
runTest {
|
||||
val uiStateFlow = MutableStateFlow(initialUiState)
|
||||
every { stateController.uiState } returns uiStateFlow
|
||||
every {
|
||||
stakingOperationsFactory.createFeeLoader(
|
||||
cryptoCurrencyStatus = any(),
|
||||
userWallet = any(),
|
||||
integration = any()
|
||||
)
|
||||
} returns mockk {
|
||||
coEvery {
|
||||
getFee(any(), any(), any(), any())
|
||||
} just Runs
|
||||
}
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
val assentUiState = mockk<StakingUiState>(relaxed = true) {
|
||||
every { currentStep } returns StakingStep.Confirmation
|
||||
every { confirmationState } returns mockk<StakingStates.ConfirmationState.Data>(relaxed = true) {
|
||||
every { innerState } returns InnerConfirmationStakingState.ASSENT
|
||||
every { notifications } returns persistentListOf()
|
||||
}
|
||||
}
|
||||
uiStateFlow.value = assentUiState
|
||||
every { stateController.value } returns assentUiState
|
||||
advanceUntilIdle()
|
||||
|
||||
clearMocks(stateController, answers = false, recordedCalls = true, verificationMarks = true)
|
||||
every { stateController.update(any<Transformer<StakingUiState>>()) } just Runs
|
||||
every { stateController.update(any<(StakingUiState) -> StakingUiState>()) } just Runs
|
||||
every { stateController.value } returns assentUiState
|
||||
|
||||
model.onPrevClick()
|
||||
|
||||
verify {
|
||||
stateController.update(
|
||||
match<Transformer<StakingUiState>> {
|
||||
it is SetConfirmationStateResetAssentTransformer
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN in progress state WHEN onPrevClick THEN nothing happens`() = runTest {
|
||||
val uiStateFlow = MutableStateFlow(initialUiState)
|
||||
every { stateController.uiState } returns uiStateFlow
|
||||
every {
|
||||
stakingOperationsFactory.createFeeLoader(any(), any(), any())
|
||||
} returns mockk {
|
||||
coEvery {
|
||||
getFee(any(), any(), any(), any())
|
||||
} just Runs
|
||||
}
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
val inProgressUiState = mockk<StakingUiState>(relaxed = true) {
|
||||
every { currentStep } returns StakingStep.Confirmation
|
||||
every { confirmationState } returns mockk<StakingStates.ConfirmationState.Data>(relaxed = true) {
|
||||
every { innerState } returns InnerConfirmationStakingState.IN_PROGRESS
|
||||
}
|
||||
}
|
||||
uiStateFlow.value = inProgressUiState
|
||||
every { stateController.value } returns inProgressUiState
|
||||
advanceUntilIdle()
|
||||
|
||||
clearMocks(stateController, answers = false, recordedCalls = true, verificationMarks = true)
|
||||
|
||||
model.onPrevClick()
|
||||
|
||||
verify(exactly = 0) { stateController.update(any<Transformer<StakingUiState>>()) }
|
||||
verify(exactly = 0) { stateController.update(any<(StakingUiState) -> StakingUiState>()) }
|
||||
verify(exactly = 0) { stateController.updateAll(*anyVararg()) }
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN completed state WHEN onPrevClick THEN onNextClick called`() = runTest {
|
||||
val uiStateFlow = MutableStateFlow(initialUiState)
|
||||
every { stateController.uiState } returns uiStateFlow
|
||||
every {
|
||||
stakingOperationsFactory.createFeeLoader(any(), any(), any())
|
||||
} returns mockk {
|
||||
coEvery {
|
||||
getFee(any(), any(), any(), any())
|
||||
} just Runs
|
||||
}
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
val completedUiState = mockk<StakingUiState>(relaxed = true) {
|
||||
every { currentStep } returns StakingStep.Confirmation
|
||||
every { confirmationState } returns mockk<StakingStates.ConfirmationState.Data>(relaxed = true) {
|
||||
every { innerState } returns InnerConfirmationStakingState.COMPLETED
|
||||
}
|
||||
}
|
||||
uiStateFlow.value = completedUiState
|
||||
every { stateController.value } returns completedUiState
|
||||
advanceUntilIdle()
|
||||
|
||||
clearMocks(stateController, answers = false, recordedCalls = true, verificationMarks = true)
|
||||
every { stateController.value } returns completedUiState
|
||||
every { stateController.update(any<(StakingUiState) -> StakingUiState>()) } just Runs
|
||||
every { stateController.clear() } just Runs
|
||||
|
||||
model.onPrevClick()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
stateController.update(any<(StakingUiState) -> StakingUiState>())
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN non-confirmation step WHEN onPrevClick THEN stakingStateRouter onPrevClick called`() = runTest {
|
||||
val uiStateFlow = MutableStateFlow(initialUiState)
|
||||
every { stateController.uiState } returns uiStateFlow
|
||||
every { stateController.value } returns initialUiState
|
||||
every { stateController.update(any<(StakingUiState) -> StakingUiState>()) } just Runs
|
||||
every { appRouter.pop(any()) } just Runs
|
||||
every { stateController.clear() } just Runs
|
||||
|
||||
val amountUiState = mockk<StakingUiState>(relaxed = true) {
|
||||
every { currentStep } returns StakingStep.Amount
|
||||
}
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
uiStateFlow.value = amountUiState
|
||||
every { stateController.value } returns amountUiState
|
||||
every { stateController.uiState } returns MutableStateFlow(amountUiState)
|
||||
|
||||
model.onPrevClick()
|
||||
|
||||
verify { stateController.update(any<(StakingUiState) -> StakingUiState>()) }
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN onRefreshSwipe true THEN loading set and balanceUpdater partialUpdate called`() = runTest {
|
||||
val testAppScope = object : AppCoroutineScope,
|
||||
CoroutineScope by this {}
|
||||
|
||||
val model = createModel(
|
||||
testScope = this,
|
||||
coroutineScope = testAppScope,
|
||||
)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onRefreshSwipe(isRefreshing = true)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
stateController.update(
|
||||
match<Transformer<StakingUiState>> {
|
||||
it is SetInitialLoadingStateTransformer
|
||||
}
|
||||
)
|
||||
}
|
||||
coVerify { mockBalanceUpdater.partialUpdate() }
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN onInitialInfoBannerClick THEN analytics sent and url opened`() = runTest {
|
||||
val expectedUrl = "https://tangem.com/blog/post/how-to-stake-cryptocurrency/?utm_source=tangem-app"
|
||||
mockkObject(TangemBlogUrlBuilder)
|
||||
coEvery { TangemBlogUrlBuilder.build(TangemBlogUrlBuilder.Post.HowToStake) } returns expectedUrl
|
||||
every { innerRouter.openUrl(any()) } just Runs
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onInitialInfoBannerClick()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
analyticsEventHandler.send(match { it is StakingAnalyticsEvent.WhatIsStaking })
|
||||
}
|
||||
verify { innerRouter.openUrl(expectedUrl) }
|
||||
|
||||
model.onDestroy()
|
||||
unmockkObject(TangemBlogUrlBuilder)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN onInfoClick THEN ShowInfoBottomSheetStateTransformer applied`() = runTest {
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onInfoClick(InfoType.ANNUAL_PERCENTAGE_RATE)
|
||||
|
||||
verify {
|
||||
stateController.update(
|
||||
match<Transformer<StakingUiState>> {
|
||||
it is ShowInfoBottomSheetStateTransformer
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN empty preferredTargets WHEN onAmountEnterClick THEN noAvailableValidators alert sent`() = runTest {
|
||||
every { testYield.preferredValidators } returns emptyList()
|
||||
every { messageSender.send(any()) } just Runs
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onAmountEnterClick()
|
||||
|
||||
verify { messageSender.send(any()) }
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN non-empty preferredTargets WHEN onAmountEnterClick THEN validator reset and onNextClick called`() =
|
||||
runTest {
|
||||
every { stateController.value } returns initialUiState
|
||||
every { testYield.preferredValidators } returns listOf(mockk(relaxed = true))
|
||||
every { initialUiState.actionType } returns StakingActionCommonType.Enter(skipEnterAmount = false)
|
||||
every { testYield.args.enter.isPartialAmountDisabled } returns true
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onAmountEnterClick()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
stateController.updateAll(
|
||||
match { it is ValidatorSelectChangeTransformer },
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN txUrl not null WHEN onExploreClick THEN analytics sent and url opened`() = runTest {
|
||||
val txUrl = "https://explorer.solana.com/tx/abc123"
|
||||
val transactionDoneState = TransactionDoneState.Content(
|
||||
timestamp = 1000L,
|
||||
txUrl = txUrl,
|
||||
)
|
||||
val confirmationState = mockk<StakingStates.ConfirmationState.Data>(relaxed = true) {
|
||||
every { this@mockk.transactionDoneState } returns transactionDoneState
|
||||
}
|
||||
val uiState = mockk<StakingUiState>(relaxed = true) {
|
||||
every { this@mockk.confirmationState } returns confirmationState
|
||||
}
|
||||
every { stateController.uiState } returns MutableStateFlow(uiState)
|
||||
every { innerRouter.openUrl(any()) } just Runs
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onExploreClick()
|
||||
|
||||
verify { analyticsEventHandler.send(match { it is StakingAnalyticsEvent.ButtonExplore }) }
|
||||
verify { innerRouter.openUrl(txUrl) }
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN txUrl not null WHEN onShareClick THEN analytics sent and shareManager called`() = runTest {
|
||||
val txUrl = "https://explorer.solana.com/tx/abc123"
|
||||
val transactionDoneState = TransactionDoneState.Content(
|
||||
timestamp = 1000L,
|
||||
txUrl = txUrl,
|
||||
)
|
||||
val confirmationState = mockk<StakingStates.ConfirmationState.Data>(relaxed = true) {
|
||||
every { this@mockk.transactionDoneState } returns transactionDoneState
|
||||
}
|
||||
val uiState = mockk<StakingUiState>(relaxed = true) {
|
||||
every { this@mockk.confirmationState } returns confirmationState
|
||||
}
|
||||
every { stateController.uiState } returns MutableStateFlow(uiState)
|
||||
every { vibratorHapticManager.performOneTime(any()) } just Runs
|
||||
every { shareManager.shareText(any()) } just Runs
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onShareClick()
|
||||
|
||||
verify { analyticsEventHandler.send(match { it is StakingAnalyticsEvent.ButtonShare }) }
|
||||
verify { vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click) }
|
||||
verify { shareManager.shareText(txUrl) }
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN onFailedTxEmailClick THEN analytics sent and sendFeedbackEmail called`() = runTest {
|
||||
coEvery { getWalletMetaInfoUseCase(userWalletId = any()) } returns Either.Right(mockk(relaxed = true))
|
||||
every { saveBlockchainErrorUseCase(error = any()) } just Runs
|
||||
coEvery { sendFeedbackEmailUseCase(type = any()) } just Runs
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onFailedTxEmailClick("test error")
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { analyticsEventHandler.send(match { it is Basic.ButtonSupport }) }
|
||||
coVerify { sendFeedbackEmailUseCase(match { it is FeedbackEmailType.StakingProblem }) }
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN openTokenDetails THEN innerRouter openTokenDetails called`() = runTest {
|
||||
every { innerRouter.openTokenDetails(any(), any()) } just Runs
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
val currency: CryptoCurrency = mockk(relaxed = true)
|
||||
model.openTokenDetails(currency)
|
||||
|
||||
verify { innerRouter.openTokenDetails(testUserWalletId, currency) }
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN showPrimaryClickAlert THEN messageSender sends alert`() = runTest {
|
||||
every { messageSender.send(any()) } just Runs
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.showPrimaryClickAlert()
|
||||
|
||||
verify { messageSender.send(any()) }
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN onOpenLearnMoreAboutApproveClick THEN urlOpener opens approve url`() = runTest {
|
||||
val expectedUrl = "https://tangem.com/blog/post/give-revoke-permission/?utm_source=tangem-app"
|
||||
mockkObject(TangemBlogUrlBuilder)
|
||||
coEvery { TangemBlogUrlBuilder.build(TangemBlogUrlBuilder.Post.GiveRevokePermission) } returns expectedUrl
|
||||
every { urlOpener.openUrl(any()) } just Runs
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onOpenLearnMoreAboutApproveClick()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { urlOpener.openUrl(expectedUrl) }
|
||||
|
||||
unmockkObject(TangemBlogUrlBuilder)
|
||||
model.onDestroy()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
package com.tangem.features.staking.impl.presentation.model
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.right
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.api.ParamsInterceptorHolder
|
||||
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.navigation.share.ShareManager
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.haptic.VibratorHapticManager
|
||||
import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus
|
||||
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||
import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase
|
||||
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
||||
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.staking.*
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
|
||||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.transaction.usecase.*
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.approval.api.GiveApprovalFeatureToggles
|
||||
import com.tangem.features.staking.api.StakingComponent
|
||||
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.StakingBalanceUpdater
|
||||
import com.tangem.features.staking.impl.presentation.state.helpers.StakingOperationsFactory
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
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(
|
||||
userWalletId = testUserWalletId,
|
||||
cryptoCurrency = testCryptoCurrency,
|
||||
integrationId = testIntegrationId,
|
||||
)
|
||||
protected val testYield: Yield = mockk(relaxed = true)
|
||||
protected val testUserWallet: UserWallet = mockk(relaxed = true)
|
||||
protected val initialUiState: StakingUiState = mockk(relaxed = true) {
|
||||
every { currentStep } returns StakingStep.InitialInfo
|
||||
}
|
||||
|
||||
private lateinit var testCryptoCurrencyStatus: CryptoCurrencyStatus
|
||||
private lateinit var testAccountCurrencyStatus: AccountCryptoCurrencyStatus
|
||||
protected lateinit var mockBalanceUpdater: StakingBalanceUpdater
|
||||
|
||||
protected val stateController: StakingStateController = mockk()
|
||||
private val getYieldUseCase: GetYieldUseCase = mockk()
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase = mockk()
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase = mockk()
|
||||
protected val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase = mockk()
|
||||
protected val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
|
||||
protected val appRouter: AppRouter = mockk()
|
||||
|
||||
protected val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase = mockk()
|
||||
protected val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase = mockk()
|
||||
protected val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase = mockk()
|
||||
protected val sendTransactionUseCase: SendTransactionUseCase = mockk()
|
||||
protected val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase = mockk()
|
||||
protected val getAllowanceUseCase: GetAllowanceUseCase = mockk()
|
||||
protected val vibratorHapticManager: VibratorHapticManager = mockk()
|
||||
protected val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase = mockk()
|
||||
protected val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase = mockk()
|
||||
protected val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase = mockk()
|
||||
protected val getCurrencyCheckUseCase: GetCurrencyCheckUseCase = mockk()
|
||||
protected val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase = mockk()
|
||||
protected val isAnyTokenStakedUseCase: IsAnyTokenStakedUseCase = mockk()
|
||||
private val invalidatePendingTransactionsUseCase: InvalidatePendingTransactionsUseCase = mockk()
|
||||
protected val stakingOperationsFactory: StakingOperationsFactory = mockk()
|
||||
protected val stakingBalanceUpdater: StakingBalanceUpdater.Factory = mockk()
|
||||
protected val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase = mockk()
|
||||
protected val getActionsUseCase: GetActionsUseCase = mockk()
|
||||
protected val p2pEthPoolRepository: P2PEthPoolRepository = mockk()
|
||||
protected val checkAccountInitializedUseCase: CheckAccountInitializedUseCase = mockk()
|
||||
protected val createTransferTransactionUseCase: CreateTransferTransactionUseCase = mockk()
|
||||
protected val getFeeUseCase: GetFeeUseCase = mockk()
|
||||
protected val getNetworkAddressesUseCase: GetNetworkAddressesUseCase = mockk()
|
||||
protected val getActionRequirementAmountUseCase: GetActionRequirementAmountUseCase = mockk()
|
||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase = mockk()
|
||||
protected val paramsInterceptorHolder: ParamsInterceptorHolder = mockk(relaxed = true)
|
||||
protected val shareManager: ShareManager = mockk()
|
||||
protected val urlOpener: UrlOpener = mockk()
|
||||
private val coroutineScope: AppCoroutineScope = mockk()
|
||||
protected val innerRouter: InnerStakingRouter = mockk()
|
||||
protected val messageSender: UiMessageSender = mockk()
|
||||
protected val giveApprovalFeatureToggles: GiveApprovalFeatureToggles = mockk()
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
MockKAnnotations.init(this)
|
||||
|
||||
val (status, accountStatus) = createMockedAccountCurrencyStatus()
|
||||
testCryptoCurrencyStatus = status
|
||||
testAccountCurrencyStatus = accountStatus
|
||||
|
||||
coEvery { getYieldUseCase(testIntegrationId.value) } returns Either.Right(testYield)
|
||||
every { getSelectedAppCurrencyUseCase() } returns flowOf(Either.Right(AppCurrency.Default))
|
||||
every { stateController.uiState } returns MutableStateFlow(initialUiState)
|
||||
every { stateController.initializeWithUserWallet(any()) } just Runs
|
||||
every { stateController.updateAll(*anyVararg()) } just Runs
|
||||
every { stateController.update(any<Transformer<StakingUiState>>()) } just Runs
|
||||
every { stateController.update(any<(StakingUiState) -> StakingUiState>()) } just Runs
|
||||
every { getUserWalletUseCase(testUserWalletId) } returns Either.Right(testUserWallet)
|
||||
every {
|
||||
getAccountCurrencyStatusUseCase(testUserWalletId, testCryptoCurrency)
|
||||
} returns flowOf(testAccountCurrencyStatus)
|
||||
coEvery { isAccountsModeEnabledUseCase.invokeSync() } returns true
|
||||
coEvery {
|
||||
getFeePaidCryptoCurrencyStatusSyncUseCase(testUserWalletId, testCryptoCurrencyStatus)
|
||||
} returns Either.Right(mockk(relaxed = true))
|
||||
coEvery {
|
||||
getMinimumTransactionAmountSyncUseCase(testUserWalletId, testCryptoCurrencyStatus)
|
||||
} returns Either.Left(mockk())
|
||||
coEvery { checkAccountInitializedUseCase(testUserWalletId, any()) } returns true.right()
|
||||
coEvery { isAnyTokenStakedUseCase(testUserWalletId) } returns Either.Right(false)
|
||||
coEvery {
|
||||
isAmountSubtractAvailableUseCase(testUserWalletId, any())
|
||||
} returns Either.Right(false)
|
||||
every { getActionsUseCase(testUserWalletId, any()) } returns emptyFlow()
|
||||
every { getBalanceHidingSettingsUseCase() } returns emptyFlow()
|
||||
mockBalanceUpdater = mockk {
|
||||
coEvery { partialUpdate() } just Runs
|
||||
}
|
||||
every {
|
||||
stakingBalanceUpdater.create(any(), any(), any())
|
||||
} returns mockBalanceUpdater
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
protected fun createModel(
|
||||
testScope: TestScope,
|
||||
paramsContainer: ParamsContainer = MutableParamsContainer(testParams),
|
||||
coroutineScope: AppCoroutineScope = this.coroutineScope,
|
||||
): StakingModel {
|
||||
return StakingModel(
|
||||
paramsContainer = paramsContainer,
|
||||
stateController = stateController,
|
||||
dispatchers = testScope.createTestingCoroutineDispatcherProvider(),
|
||||
getBalanceHidingSettingsUseCase = getBalanceHidingSettingsUseCase,
|
||||
getFeePaidCryptoCurrencyStatusSyncUseCase = getFeePaidCryptoCurrencyStatusSyncUseCase,
|
||||
getMinimumTransactionAmountSyncUseCase = getMinimumTransactionAmountSyncUseCase,
|
||||
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
|
||||
getUserWalletUseCase = getUserWalletUseCase,
|
||||
sendTransactionUseCase = sendTransactionUseCase,
|
||||
createApprovalTransactionUseCase = createApprovalTransactionUseCase,
|
||||
getAllowanceUseCase = getAllowanceUseCase,
|
||||
vibratorHapticManager = vibratorHapticManager,
|
||||
getWalletMetaInfoUseCase = getWalletMetaInfoUseCase,
|
||||
saveBlockchainErrorUseCase = saveBlockchainErrorUseCase,
|
||||
getBalanceNotEnoughForFeeWarningUseCase = getBalanceNotEnoughForFeeWarningUseCase,
|
||||
getCurrencyCheckUseCase = getCurrencyCheckUseCase,
|
||||
isAmountSubtractAvailableUseCase = isAmountSubtractAvailableUseCase,
|
||||
isAnyTokenStakedUseCase = isAnyTokenStakedUseCase,
|
||||
invalidatePendingTransactionsUseCase = invalidatePendingTransactionsUseCase,
|
||||
stakingOperationsFactory = stakingOperationsFactory,
|
||||
stakingBalanceUpdater = stakingBalanceUpdater,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
sendFeedbackEmailUseCase = sendFeedbackEmailUseCase,
|
||||
getActionsUseCase = getActionsUseCase,
|
||||
getYieldUseCase = getYieldUseCase,
|
||||
p2pEthPoolRepository = p2pEthPoolRepository,
|
||||
checkAccountInitializedUseCase = checkAccountInitializedUseCase,
|
||||
createTransferTransactionUseCase = createTransferTransactionUseCase,
|
||||
getFeeUseCase = getFeeUseCase,
|
||||
getNetworkAddressesUseCase = getNetworkAddressesUseCase,
|
||||
getActionRequirementAmountUseCase = getActionRequirementAmountUseCase,
|
||||
isAccountsModeEnabledUseCase = isAccountsModeEnabledUseCase,
|
||||
getAccountCurrencyStatusUseCase = getAccountCurrencyStatusUseCase,
|
||||
paramsInterceptorHolder = paramsInterceptorHolder,
|
||||
shareManager = shareManager,
|
||||
urlOpener = urlOpener,
|
||||
coroutineScope = coroutineScope,
|
||||
innerRouter = innerRouter,
|
||||
messageSender = messageSender,
|
||||
giveApprovalFeatureToggles = giveApprovalFeatureToggles,
|
||||
appRouter = appRouter,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TestScope.createTestingCoroutineDispatcherProvider(): TestingCoroutineDispatcherProvider {
|
||||
val testDispatcher = StandardTestDispatcher(testScheduler)
|
||||
return TestingCoroutineDispatcherProvider(
|
||||
main = testDispatcher,
|
||||
mainImmediate = testDispatcher,
|
||||
io = testDispatcher,
|
||||
default = testDispatcher,
|
||||
single = testDispatcher,
|
||||
)
|
||||
}
|
||||
|
||||
protected fun createMockedAccountCurrencyStatus(): Pair<CryptoCurrencyStatus, AccountCryptoCurrencyStatus> {
|
||||
val testCryptoCurrencyStatus: CryptoCurrencyStatus = mockk(relaxed = true)
|
||||
val testAccountCurrencyStatus = mockk<AccountCryptoCurrencyStatus> {
|
||||
every { component1() } returns mockk(relaxed = true)
|
||||
every { component2() } returns testCryptoCurrencyStatus
|
||||
}
|
||||
return testCryptoCurrencyStatus to testAccountCurrencyStatus
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,792 @@
|
|||
package com.tangem.features.staking.impl.presentation.model
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent
|
||||
import com.tangem.domain.staking.model.StakingApproval
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.features.staking.api.StakingComponent
|
||||
import com.tangem.features.staking.impl.presentation.state.*
|
||||
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.StakingTransactionSender
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.SetConfirmationStateInProgressTransformer
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.SetConfirmationStateLoadingTransformer
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.SetConfirmationStateResetAssentTransformer
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetApprovalBottomSheetInProgressTransformer
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetApprovalBottomSheetTypeChangeTransformer
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.approval.ShowApprovalBottomSheetTransformer
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.ton.CompleteInitializeBottomSheetTransformer
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.ton.SetFeeErrorToTonInitializeBottomSheetTransformer
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.ton.SetFeeToTonInitializeBottomSheetTransformer
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.ton.ShowTonInitializeBottomSheetTransformer
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class StakingModelTransactionTest : StakingModelTestBase() {
|
||||
|
||||
@Test
|
||||
fun `WHEN getFee THEN loading state set and feeLoader called`() = runTest {
|
||||
val mockFeeLoader = mockk<StakingFeeLoader> {
|
||||
coEvery {
|
||||
getFee(any(), any(), any(), any())
|
||||
} just Runs
|
||||
}
|
||||
every {
|
||||
stakingOperationsFactory.createFeeLoader(any(), any(), any())
|
||||
} returns mockFeeLoader
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.getFee()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
stateController.update(
|
||||
transformer = match<Transformer<StakingUiState>> {
|
||||
it is SetConfirmationStateLoadingTransformer
|
||||
}
|
||||
)
|
||||
}
|
||||
coVerify {
|
||||
mockFeeLoader.getFee(any(), any(), any(), any())
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN StakeKit integration AND assent state WHEN onActionClick THEN sendTransaction called`() = runTest {
|
||||
val uiStateFlow = MutableStateFlow(initialUiState)
|
||||
every { stateController.uiState } returns uiStateFlow
|
||||
val mockFeeLoader = mockk<StakingFeeLoader> {
|
||||
coEvery {
|
||||
getFee(any(), any(), any(), any())
|
||||
} just Runs
|
||||
}
|
||||
every {
|
||||
stakingOperationsFactory.createFeeLoader(any(), any(), any())
|
||||
} returns mockFeeLoader
|
||||
val mockTransactionSender = mockk<StakingTransactionSender> {
|
||||
coEvery { send(any()) } just Runs
|
||||
}
|
||||
every {
|
||||
stakingOperationsFactory.createTransactionSender(
|
||||
cryptoCurrencyStatus = any(),
|
||||
userWallet = any(),
|
||||
integration = any(),
|
||||
isAmountSubtractAvailable = any()
|
||||
)
|
||||
} returns mockTransactionSender
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
val assentUiState = mockk<StakingUiState>(relaxed = true) {
|
||||
every { currentStep } returns StakingStep.Confirmation
|
||||
every { confirmationState } returns mockk<StakingStates.ConfirmationState.Data> {
|
||||
every { innerState } returns InnerConfirmationStakingState.ASSENT
|
||||
}
|
||||
}
|
||||
uiStateFlow.value = assentUiState
|
||||
every { stateController.value } returns assentUiState
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onActionClick()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
stateController.update(
|
||||
transformer = match<Transformer<StakingUiState>> {
|
||||
it is SetConfirmationStateInProgressTransformer
|
||||
}
|
||||
)
|
||||
}
|
||||
coVerify { mockTransactionSender.send(any()) }
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN P2PEthPool AND fee not increased WHEN onActionClick THEN sendTransaction called directly`() = runTest {
|
||||
val p2pParams = StakingComponent.Params(
|
||||
userWalletId = testUserWalletId,
|
||||
cryptoCurrency = testCryptoCurrency,
|
||||
integrationId = StakingIntegrationID.P2PEthPool,
|
||||
)
|
||||
coEvery { p2pEthPoolRepository.getVaultsSync() } returns emptyList()
|
||||
val uiStateFlow = MutableStateFlow(initialUiState)
|
||||
every { stateController.uiState } returns uiStateFlow
|
||||
coEvery {
|
||||
getBalanceNotEnoughForFeeWarningUseCase(
|
||||
fee = any(),
|
||||
userWalletId = any(),
|
||||
tokenStatus = any(),
|
||||
feeStatus = any()
|
||||
)
|
||||
} returns Either.Right(null)
|
||||
coEvery {
|
||||
getCurrencyCheckUseCase(
|
||||
userWalletId = any(),
|
||||
currencyStatus = any(),
|
||||
feeCurrencyStatus = any(),
|
||||
amount = any(),
|
||||
fee = any(),
|
||||
feeCurrencyBalanceAfterTransaction = any(),
|
||||
recipientAddress = any()
|
||||
)
|
||||
} returns mockk(relaxed = true)
|
||||
val newFee = mockk<Fee.Common>(relaxed = true) {
|
||||
every { amount.value } returns BigDecimal.ONE
|
||||
}
|
||||
val mockFeeLoader = mockk<StakingFeeLoader> {
|
||||
coEvery {
|
||||
getFee(any(), any(), any(), any())
|
||||
} coAnswers {
|
||||
firstArg<(Fee, Boolean) -> Unit>().invoke(newFee, false)
|
||||
}
|
||||
}
|
||||
every {
|
||||
stakingOperationsFactory.createFeeLoader(any(), any(), any())
|
||||
} returns mockFeeLoader
|
||||
val mockTransactionSender = mockk<StakingTransactionSender> {
|
||||
coEvery { send(any()) } just Runs
|
||||
}
|
||||
every {
|
||||
stakingOperationsFactory.createTransactionSender(
|
||||
cryptoCurrencyStatus = any(),
|
||||
userWallet = any(),
|
||||
integration = any(),
|
||||
isAmountSubtractAvailable = any()
|
||||
)
|
||||
} returns mockTransactionSender
|
||||
val currentFee = mockk<Fee.Common>(relaxed = true) {
|
||||
every { amount.value } returns BigDecimal.TEN
|
||||
}
|
||||
val assentUiState = mockk<StakingUiState>(relaxed = true) {
|
||||
every { currentStep } returns StakingStep.Confirmation
|
||||
every { confirmationState } returns mockk<StakingStates.ConfirmationState.Data>(relaxed = true) {
|
||||
every { innerState } returns InnerConfirmationStakingState.ASSENT
|
||||
every { feeState } returns mockk<FeeState.Content>(relaxed = true) {
|
||||
every { fee } returns currentFee
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val model = createModel(
|
||||
paramsContainer = MutableParamsContainer(p2pParams),
|
||||
testScope = this,
|
||||
)
|
||||
advanceUntilIdle()
|
||||
|
||||
uiStateFlow.value = assentUiState
|
||||
every { stateController.value } returns assentUiState
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onActionClick()
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify { mockTransactionSender.send(any()) }
|
||||
verify(exactly = 0) { messageSender.send(match { it is DialogMessage }) }
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN P2PEthPool AND fee increased WHEN onActionClick THEN fee updated alert shown`() = runTest {
|
||||
val p2pParams = StakingComponent.Params(
|
||||
userWalletId = testUserWalletId,
|
||||
cryptoCurrency = testCryptoCurrency,
|
||||
integrationId = StakingIntegrationID.P2PEthPool,
|
||||
)
|
||||
coEvery { p2pEthPoolRepository.getVaultsSync() } returns emptyList()
|
||||
val uiStateFlow = MutableStateFlow(initialUiState)
|
||||
every { stateController.uiState } returns uiStateFlow
|
||||
coEvery {
|
||||
getBalanceNotEnoughForFeeWarningUseCase(
|
||||
fee = any(),
|
||||
userWalletId = any(),
|
||||
tokenStatus = any(),
|
||||
feeStatus = any()
|
||||
)
|
||||
} returns Either.Right(null)
|
||||
coEvery {
|
||||
getCurrencyCheckUseCase(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any()
|
||||
)
|
||||
} returns mockk(relaxed = true)
|
||||
every { messageSender.send(any()) } just Runs
|
||||
val newFee = mockk<Fee.Common>(relaxed = true) {
|
||||
every { amount.value } returns BigDecimal.TEN
|
||||
}
|
||||
val mockFeeLoader = mockk<StakingFeeLoader> {
|
||||
coEvery {
|
||||
getFee(any(), any(), any(), any())
|
||||
} coAnswers {
|
||||
firstArg<(Fee, Boolean) -> Unit>().invoke(newFee, false)
|
||||
}
|
||||
}
|
||||
every {
|
||||
stakingOperationsFactory.createFeeLoader(any(), any(), any())
|
||||
} returns mockFeeLoader
|
||||
val mockTransactionSender = mockk<StakingTransactionSender> {
|
||||
coEvery { send(any()) } just Runs
|
||||
}
|
||||
every {
|
||||
stakingOperationsFactory.createTransactionSender(
|
||||
cryptoCurrencyStatus = any(),
|
||||
userWallet = any(),
|
||||
integration = any(),
|
||||
isAmountSubtractAvailable = any(),
|
||||
)
|
||||
} returns mockTransactionSender
|
||||
val currentFee = mockk<Fee.Common>(relaxed = true) {
|
||||
every { amount.value } returns BigDecimal.ONE
|
||||
}
|
||||
val assentUiState = mockk<StakingUiState>(relaxed = true) {
|
||||
every { currentStep } returns StakingStep.Confirmation
|
||||
every { confirmationState } returns mockk<StakingStates.ConfirmationState.Data>(relaxed = true) {
|
||||
every { innerState } returns InnerConfirmationStakingState.ASSENT
|
||||
every { feeState } returns mockk<FeeState.Content>(relaxed = true) {
|
||||
every { fee } returns currentFee
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val model = createModel(
|
||||
paramsContainer = MutableParamsContainer(p2pParams),
|
||||
testScope = this,
|
||||
)
|
||||
advanceUntilIdle()
|
||||
|
||||
uiStateFlow.value = assentUiState
|
||||
every { stateController.value } returns assentUiState
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onActionClick()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
stateController.update(
|
||||
match<Transformer<StakingUiState>> {
|
||||
it is SetConfirmationStateResetAssentTransformer
|
||||
},
|
||||
)
|
||||
}
|
||||
verify { messageSender.send(any()) }
|
||||
coVerify(exactly = 0) { mockTransactionSender.send(any()) }
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN gasless approval enabled WHEN showApprovalBottomSheet THEN approvalSlotNavigation activated`() =
|
||||
runTest {
|
||||
every { giveApprovalFeatureToggles.isGaslessApprovalEnabled } returns true
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.showApprovalBottomSheet()
|
||||
|
||||
verify(exactly = 0) {
|
||||
stateController.update(
|
||||
transformer = match<Transformer<StakingUiState>> { it is ShowApprovalBottomSheetTransformer }
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN gasless disabled WHEN showApprovalBottomSheet THEN ShowApprovalBottomSheetTransformer applied`() =
|
||||
runTest {
|
||||
every { giveApprovalFeatureToggles.isGaslessApprovalEnabled } returns false
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.showApprovalBottomSheet()
|
||||
|
||||
verify {
|
||||
stateController.update(
|
||||
transformer = match<Transformer<StakingUiState>> { it is ShowApprovalBottomSheetTransformer }
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN onApproveTypeChange THEN SetApprovalBottomSheetTypeChangeTransformer applied`() = runTest {
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onApproveTypeChange(ApproveType.LIMITED)
|
||||
|
||||
verify {
|
||||
stateController.update(
|
||||
transformer = match<Transformer<StakingUiState>> { it is SetApprovalBottomSheetTypeChangeTransformer },
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN approval needed WHEN onApprovalClick THEN in progress set and createApprovalTransaction called`() =
|
||||
runTest {
|
||||
val spenderAddress = "0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908"
|
||||
val expectedNetwork = mockk<Network> {
|
||||
every { name } returns "KEK"
|
||||
}
|
||||
val testToken: CryptoCurrency.Token = mockk(relaxed = true) {
|
||||
every { network } returns expectedNetwork
|
||||
}
|
||||
val testCryptoCurrencyStatus: CryptoCurrencyStatus = mockk(relaxed = true) {
|
||||
every { currency } returns testToken
|
||||
}
|
||||
val testAccountCurrencyStatus = mockk<AccountCryptoCurrencyStatus> {
|
||||
every { component1() } returns mockk(relaxed = true)
|
||||
every { component2() } returns testCryptoCurrencyStatus
|
||||
}
|
||||
every {
|
||||
getAccountCurrencyStatusUseCase(testUserWalletId, testCryptoCurrency)
|
||||
} returns flowOf(testAccountCurrencyStatus)
|
||||
coEvery {
|
||||
getFeePaidCryptoCurrencyStatusSyncUseCase(testUserWalletId, testCryptoCurrencyStatus)
|
||||
} returns Either.Right(mockk(relaxed = true))
|
||||
coEvery {
|
||||
getMinimumTransactionAmountSyncUseCase(testUserWalletId, testCryptoCurrencyStatus)
|
||||
} returns Either.Left(mockk())
|
||||
|
||||
// Setup stakingApproval = Needed
|
||||
mockkObject(StakingIntegrationID.Companion)
|
||||
every {
|
||||
StakingIntegrationID.create(any())
|
||||
} returns mockk {
|
||||
every { approval } returns StakingApproval.Needed(spenderAddress)
|
||||
}
|
||||
coEvery {
|
||||
getAllowanceUseCase(testUserWalletId, any(), spenderAddress)
|
||||
} returns Either.Right(BigDecimal.TEN)
|
||||
|
||||
every {
|
||||
stakingOperationsFactory.createFeeLoader(
|
||||
cryptoCurrencyStatus = any(),
|
||||
userWallet = any(),
|
||||
integration = any()
|
||||
)
|
||||
} returns mockk<StakingFeeLoader> {
|
||||
coEvery {
|
||||
getFee(
|
||||
onStakingFee = any(),
|
||||
onStakingFeeError = any(),
|
||||
onApprovalFee = any(),
|
||||
onFeeError = any()
|
||||
)
|
||||
} just Runs
|
||||
}
|
||||
val expectedApprovalTx = Either.Right<TransactionData.Uncompiled>(mockk(relaxed = true))
|
||||
coEvery {
|
||||
createApprovalTransactionUseCase.invoke(
|
||||
cryptoCurrencyStatus = any(),
|
||||
userWalletId = any(),
|
||||
amount = any(),
|
||||
fee = any(),
|
||||
contractAddress = any(),
|
||||
spenderAddress = any(),
|
||||
)
|
||||
} returns expectedApprovalTx
|
||||
coEvery {
|
||||
sendTransactionUseCase(any(), any(), any())
|
||||
} returns Either.Right("txHash")
|
||||
every { vibratorHapticManager.performOneTime(any()) } just Runs
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Now override stateController.value with confirmation state after cryptoCurrencyStatus is initialized
|
||||
val testFee: Fee.Common = mockk(relaxed = true)
|
||||
val confirmationState = mockk<StakingStates.ConfirmationState.Data>(relaxed = true) {
|
||||
every { feeState } returns mockk<FeeState.Content>(relaxed = true) {
|
||||
every { fee } returns testFee
|
||||
}
|
||||
}
|
||||
val uiState = mockk<StakingUiState>(relaxed = true) {
|
||||
every { this@mockk.confirmationState } returns confirmationState
|
||||
every { bottomSheetConfig } returns null
|
||||
}
|
||||
every { stateController.value } returns uiState
|
||||
|
||||
model.onApprovalClick()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
stateController.update(
|
||||
transformer = match<Transformer<StakingUiState>> {
|
||||
it is SetApprovalBottomSheetInProgressTransformer
|
||||
},
|
||||
)
|
||||
}
|
||||
coVerify {
|
||||
sendTransactionUseCase(
|
||||
txData = expectedApprovalTx.value,
|
||||
userWallet = testUserWallet,
|
||||
network = expectedNetwork,
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
unmockkObject(StakingIntegrationID.Companion)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN approval needed AND amountState data WHEN getApprovalParams THEN returns non-null params`() = runTest {
|
||||
val spenderAddress = "0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908"
|
||||
|
||||
mockkObject(StakingIntegrationID.Companion)
|
||||
try {
|
||||
every {
|
||||
StakingIntegrationID.create(any())
|
||||
} returns mockk {
|
||||
every { approval } returns StakingApproval.Needed(spenderAddress)
|
||||
}
|
||||
coEvery {
|
||||
getAllowanceUseCase(testUserWalletId, any(), spenderAddress)
|
||||
} returns Either.Right(BigDecimal.TEN)
|
||||
|
||||
val amountState = mockk<AmountState.Data>(relaxed = true) {
|
||||
every { amountTextField.value } returns "100"
|
||||
}
|
||||
val uiState = mockk<StakingUiState>(relaxed = true) {
|
||||
every { this@mockk.amountState } returns amountState
|
||||
}
|
||||
every { stateController.value } returns uiState
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
val result = model.getApprovalParams()
|
||||
|
||||
assert(result != null) { "Expected non-null GiveApprovalComponent.Params" }
|
||||
assert(result!!.spenderAddress == spenderAddress) {
|
||||
"Expected spenderAddress=$spenderAddress, got=${result.spenderAddress}"
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
} finally {
|
||||
unmockkObject(StakingIntegrationID.Companion)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN getFee returns Left WHEN onActivateTonAccountNotificationClick THEN fee error transformer applied`() =
|
||||
runTest {
|
||||
val (testCryptoCurrencyStatus, testAccountCurrencyStatus) = createMockedAccountCurrencyStatus()
|
||||
every { testCryptoCurrencyStatus.currency } returns mockk {
|
||||
every { id } returns CryptoCurrency.ID.fromValue("coin⟨ethereum→-1843072795⟩ethereum")
|
||||
every { symbol } returns "KEK"
|
||||
every { network } returns mockk()
|
||||
every { decimals } returns 2
|
||||
}
|
||||
every {
|
||||
getAccountCurrencyStatusUseCase(testUserWalletId, testCryptoCurrency)
|
||||
} returns flowOf(testAccountCurrencyStatus)
|
||||
coEvery {
|
||||
getFeePaidCryptoCurrencyStatusSyncUseCase(testUserWalletId, testCryptoCurrencyStatus)
|
||||
} returns Either.Right(mockk(relaxed = true))
|
||||
coEvery {
|
||||
getMinimumTransactionAmountSyncUseCase(testUserWalletId, testCryptoCurrencyStatus)
|
||||
} returns Either.Left(mockk())
|
||||
coEvery {
|
||||
getNetworkAddressesUseCase.invokeSync(
|
||||
userWalletId = any(),
|
||||
network = any<Network>()
|
||||
)
|
||||
} returns listOf(mockk(relaxed = true) { every { address } returns "TON_ADDRESS" })
|
||||
coEvery {
|
||||
createTransferTransactionUseCase(
|
||||
amount = any(), memo = any(), destination = any(),
|
||||
userWalletId = any(), network = any(),
|
||||
)
|
||||
} returns Either.Right(mockk(relaxed = true))
|
||||
coEvery {
|
||||
getFeeUseCase(userWallet = any(), network = any(), transactionData = any())
|
||||
} returns Either.Left(mockk(relaxed = true))
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onActivateTonAccountNotificationClick()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
analyticsEventHandler.send(StakingAnalyticsEvent.UninitializedAddressScreen("KEK"))
|
||||
}
|
||||
verify {
|
||||
stateController.update(
|
||||
transformer = match<Transformer<StakingUiState>> { it is ShowTonInitializeBottomSheetTransformer }
|
||||
)
|
||||
}
|
||||
verify {
|
||||
stateController.update(
|
||||
transformer = match<Transformer<StakingUiState>> {
|
||||
it is SetFeeErrorToTonInitializeBottomSheetTransformer
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN getFee returns Right WHEN onActivateTonAccountNotificationClick THEN fee transformer applied`() =
|
||||
runTest {
|
||||
val (testCryptoCurrencyStatus, testAccountCurrencyStatus) = createMockedAccountCurrencyStatus()
|
||||
every { testCryptoCurrencyStatus.currency } returns mockk {
|
||||
every { id } returns CryptoCurrency.ID.fromValue("coin⟨ethereum→-1843072795⟩ethereum")
|
||||
every { symbol } returns "SHMEK"
|
||||
every { network } returns mockk()
|
||||
every { decimals } returns 2
|
||||
}
|
||||
every {
|
||||
getAccountCurrencyStatusUseCase(testUserWalletId, testCryptoCurrency)
|
||||
} returns flowOf(testAccountCurrencyStatus)
|
||||
coEvery {
|
||||
getFeePaidCryptoCurrencyStatusSyncUseCase(testUserWalletId, testCryptoCurrencyStatus)
|
||||
} returns Either.Right(mockk(relaxed = true))
|
||||
coEvery {
|
||||
getMinimumTransactionAmountSyncUseCase(testUserWalletId, testCryptoCurrencyStatus)
|
||||
} returns Either.Left(mockk())
|
||||
coEvery {
|
||||
getNetworkAddressesUseCase.invokeSync(
|
||||
userWalletId = any(),
|
||||
network = any<Network>()
|
||||
)
|
||||
} returns listOf(mockk(relaxed = true) { every { address } returns "TON_ADDRESS" })
|
||||
coEvery {
|
||||
createTransferTransactionUseCase(
|
||||
amount = any(), memo = any(), destination = any(),
|
||||
userWalletId = any(), network = any(),
|
||||
)
|
||||
} returns Either.Right(mockk(relaxed = true))
|
||||
coEvery {
|
||||
getFeeUseCase(userWallet = any(), network = any(), transactionData = any())
|
||||
} returns Either.Right(mockk(relaxed = true))
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onActivateTonAccountNotificationClick()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
analyticsEventHandler.send(StakingAnalyticsEvent.UninitializedAddressScreen("SHMEK"))
|
||||
}
|
||||
verify {
|
||||
stateController.update(
|
||||
transformer = match<Transformer<StakingUiState>> { it is ShowTonInitializeBottomSheetTransformer }
|
||||
)
|
||||
}
|
||||
verify {
|
||||
stateController.update(
|
||||
match<Transformer<StakingUiState>> {
|
||||
it is SetFeeToTonInitializeBottomSheetTransformer
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN onActivateTonAccountNotificationShow THEN UninitializedAddress analytics sent with token`() = runTest {
|
||||
val (testCryptoCurrencyStatus, testAccountCurrencyStatus) = createMockedAccountCurrencyStatus()
|
||||
every { testCryptoCurrencyStatus.currency.symbol } returns "TON"
|
||||
every {
|
||||
getAccountCurrencyStatusUseCase(testUserWalletId, testCryptoCurrency)
|
||||
} returns flowOf(testAccountCurrencyStatus)
|
||||
coEvery {
|
||||
getFeePaidCryptoCurrencyStatusSyncUseCase(testUserWalletId, testCryptoCurrencyStatus)
|
||||
} returns Either.Right(mockk(relaxed = true))
|
||||
coEvery {
|
||||
getMinimumTransactionAmountSyncUseCase(testUserWalletId, testCryptoCurrencyStatus)
|
||||
} returns Either.Left(mockk())
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onActivateTonAccountNotificationShow()
|
||||
|
||||
verify {
|
||||
analyticsEventHandler.send(StakingAnalyticsEvent.UninitializedAddress(token = "TON"))
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN onNotEnoughFeeNotificationShow THEN NotEnoughFee analytics sent with token`() = runTest {
|
||||
val (testCryptoCurrencyStatus, testAccountCurrencyStatus) = createMockedAccountCurrencyStatus()
|
||||
every { testCryptoCurrencyStatus.currency.symbol } returns "SOL"
|
||||
every {
|
||||
getAccountCurrencyStatusUseCase(testUserWalletId, testCryptoCurrency)
|
||||
} returns flowOf(testAccountCurrencyStatus)
|
||||
coEvery {
|
||||
getFeePaidCryptoCurrencyStatusSyncUseCase(testUserWalletId, testCryptoCurrencyStatus)
|
||||
} returns Either.Right(mockk(relaxed = true))
|
||||
coEvery {
|
||||
getMinimumTransactionAmountSyncUseCase(testUserWalletId, testCryptoCurrencyStatus)
|
||||
} returns Either.Left(mockk())
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onNotEnoughFeeNotificationShow()
|
||||
|
||||
verify {
|
||||
analyticsEventHandler.send(StakingAnalyticsEvent.NotEnoughFee(token = "SOL"))
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN sendTransaction returns Left WHEN onActivateTonAccountClick THEN error dialog sent`() = runTest {
|
||||
val (testCryptoCurrencyStatus, testAccountCurrencyStatus) = createMockedAccountCurrencyStatus()
|
||||
every { testCryptoCurrencyStatus.currency.symbol } returns "TON"
|
||||
every {
|
||||
getAccountCurrencyStatusUseCase(testUserWalletId, testCryptoCurrency)
|
||||
} returns flowOf(testAccountCurrencyStatus)
|
||||
coEvery {
|
||||
getFeePaidCryptoCurrencyStatusSyncUseCase(testUserWalletId, testCryptoCurrencyStatus)
|
||||
} returns Either.Right(mockk(relaxed = true))
|
||||
coEvery {
|
||||
getMinimumTransactionAmountSyncUseCase(testUserWalletId, testCryptoCurrencyStatus)
|
||||
} returns Either.Left(mockk())
|
||||
coEvery {
|
||||
getNetworkAddressesUseCase.invokeSync(
|
||||
userWalletId = any(),
|
||||
network = any<Network>()
|
||||
)
|
||||
} returns listOf(mockk(relaxed = true) { every { address } returns "TON_ADDRESS" })
|
||||
coEvery {
|
||||
createTransferTransactionUseCase(
|
||||
amount = any(), memo = any(), destination = any(),
|
||||
userWalletId = any(), network = any(),
|
||||
)
|
||||
} returns Either.Right(mockk(relaxed = true))
|
||||
coEvery {
|
||||
getFeeUseCase(userWallet = any(), network = any(), transactionData = any())
|
||||
} returns Either.Right(mockk(relaxed = true))
|
||||
coEvery {
|
||||
sendTransactionUseCase(any(), any(), any())
|
||||
} returns Either.Left(mockk(relaxed = true))
|
||||
every { messageSender.send(any()) } just Runs
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
// First populate tonAccountInitializeTransaction
|
||||
model.onActivateTonAccountNotificationClick()
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onActivateTonAccountClick()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
analyticsEventHandler.send(StakingAnalyticsEvent.ButtonActivate(token = "TON"))
|
||||
}
|
||||
verify { messageSender.send(any()) }
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN sendTransaction returns Right WHEN onActivateTonAccountClick THEN complete transformer applied`() =
|
||||
runTest {
|
||||
val (testCryptoCurrencyStatus, testAccountCurrencyStatus) = createMockedAccountCurrencyStatus()
|
||||
every { testCryptoCurrencyStatus.currency.symbol } returns "TON"
|
||||
every {
|
||||
getAccountCurrencyStatusUseCase(testUserWalletId, testCryptoCurrency)
|
||||
} returns flowOf(testAccountCurrencyStatus)
|
||||
coEvery {
|
||||
getFeePaidCryptoCurrencyStatusSyncUseCase(testUserWalletId, testCryptoCurrencyStatus)
|
||||
} returns Either.Right(mockk(relaxed = true))
|
||||
coEvery {
|
||||
getMinimumTransactionAmountSyncUseCase(testUserWalletId, testCryptoCurrencyStatus)
|
||||
} returns Either.Left(mockk())
|
||||
val mockBalanceUpdater: StakingBalanceUpdater = mockk {
|
||||
coEvery { partialUpdate() } just Runs
|
||||
coEvery { partialUpdateWithDelay() } just Runs
|
||||
}
|
||||
every {
|
||||
stakingBalanceUpdater.create(any(), any(), any())
|
||||
} returns mockBalanceUpdater
|
||||
coEvery {
|
||||
getNetworkAddressesUseCase.invokeSync(
|
||||
userWalletId = any(),
|
||||
network = any<Network>()
|
||||
)
|
||||
} returns listOf(mockk(relaxed = true) { every { address } returns "TON_ADDRESS" })
|
||||
coEvery {
|
||||
createTransferTransactionUseCase(
|
||||
amount = any(), memo = any(), destination = any(),
|
||||
userWalletId = any(), network = any(),
|
||||
)
|
||||
} returns Either.Right(mockk(relaxed = true))
|
||||
coEvery {
|
||||
getFeeUseCase(userWallet = any(), network = any(), transactionData = any())
|
||||
} returns Either.Right(mockk(relaxed = true))
|
||||
coEvery {
|
||||
sendTransactionUseCase(any(), any(), any())
|
||||
} returns Either.Right("txHash")
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
// First populate tonAccountInitializeTransaction
|
||||
model.onActivateTonAccountNotificationClick()
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onActivateTonAccountClick()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
analyticsEventHandler.send(StakingAnalyticsEvent.ButtonActivate(token = "TON"))
|
||||
}
|
||||
verify {
|
||||
stateController.update(
|
||||
match<Transformer<StakingUiState>> { it is CompleteInitializeBottomSheetTransformer },
|
||||
)
|
||||
}
|
||||
coVerify { mockBalanceUpdater.partialUpdateWithDelay() }
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,351 @@
|
|||
package com.tangem.features.staking.impl.presentation.model
|
||||
|
||||
import com.tangem.domain.models.staking.BalanceType
|
||||
import com.tangem.domain.models.staking.PendingAction
|
||||
import com.tangem.domain.models.staking.PendingActionConstraints
|
||||
import com.tangem.domain.models.staking.RewardBlockType
|
||||
import com.tangem.domain.models.staking.action.StakingActionType
|
||||
import com.tangem.domain.staking.analytics.StakeScreenSource
|
||||
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent
|
||||
import com.tangem.domain.staking.model.StakingTarget
|
||||
import com.tangem.features.staking.impl.presentation.state.*
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.SetAmountDataTransformer
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.SetConfirmationStateInitTransformer
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.ShowActionSelectorBottomSheetTransformer
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.validator.ValidatorSelectChangeTransformer
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import io.mockk.*
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class StakingModelValidatorTest : StakingModelTestBase() {
|
||||
|
||||
@Test
|
||||
fun `WHEN openValidators THEN analytics sent and step changed to Validators`() = runTest {
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.openValidators()
|
||||
|
||||
verify {
|
||||
analyticsEventHandler.send(
|
||||
match { it is StakingAnalyticsEvent.ButtonValidator },
|
||||
)
|
||||
}
|
||||
verify {
|
||||
stateController.update(any<(StakingUiState) -> StakingUiState>())
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN onTargetSelect THEN analytics sent and ValidatorSelectChangeTransformer applied`() = runTest {
|
||||
val target: StakingTarget = mockk(relaxed = true) {
|
||||
every { name } returns "TestValidator"
|
||||
}
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onTargetSelect(target)
|
||||
|
||||
verify {
|
||||
analyticsEventHandler.send(event = StakingAnalyticsEvent.ValidatorChosen("TestValidator"))
|
||||
}
|
||||
verify {
|
||||
stateController.update(
|
||||
transformer = match<Transformer<StakingUiState>> { it is ValidatorSelectChangeTransformer }
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no RewardsRequirementsError AND single reward WHEN openRewardsValidators THEN onActiveStake called`() =
|
||||
runTest {
|
||||
every { stateController.value } returns initialUiState
|
||||
every { messageSender.send(any()) } just Runs
|
||||
|
||||
val singleReward: BalanceState = mockk(relaxed = true) {
|
||||
every { pendingActions } returns persistentListOf()
|
||||
}
|
||||
val yieldBalance = InnerYieldBalanceState.Data(
|
||||
integrationId = "test-integration",
|
||||
reward = YieldReward(
|
||||
rewardsCrypto = "1.0",
|
||||
rewardsFiat = "$1.00",
|
||||
rewardBlockType = RewardBlockType.Rewards,
|
||||
rewardConstraints = null,
|
||||
),
|
||||
isActionable = true,
|
||||
balances = persistentListOf(),
|
||||
)
|
||||
val initialInfoState = mockk<StakingStates.InitialInfoState.Data>(relaxed = true) {
|
||||
every { this@mockk.yieldBalance } returns yieldBalance
|
||||
}
|
||||
val rewardsValidatorsState = mockk<StakingStates.RewardsValidatorsState.Data>(relaxed = true) {
|
||||
every { rewards } returns persistentListOf(singleReward)
|
||||
}
|
||||
val uiState = mockk<StakingUiState>(relaxed = true) {
|
||||
every { this@mockk.initialInfoState } returns initialInfoState
|
||||
every { this@mockk.rewardsValidatorsState } returns rewardsValidatorsState
|
||||
}
|
||||
every { stateController.value } returns uiState
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.openRewardsValidators()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { analyticsEventHandler.send(match { it is StakingAnalyticsEvent.ButtonRewards }) }
|
||||
// onActiveStake path — ButtonValidator analytics should NOT be sent
|
||||
verify(exactly = 0) {
|
||||
analyticsEventHandler.send(match { it is StakingAnalyticsEvent.ButtonValidator })
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no RewardsRequirementsError AND rewards WHEN openRewardsValidators THEN showRewardsValidators called`() =
|
||||
runTest {
|
||||
every { stateController.value } returns initialUiState
|
||||
|
||||
val reward1: BalanceState = mockk(relaxed = true)
|
||||
val reward2: BalanceState = mockk(relaxed = true)
|
||||
val yieldBalance = InnerYieldBalanceState.Data(
|
||||
integrationId = "test-integration",
|
||||
reward = YieldReward(
|
||||
rewardsCrypto = "2.0",
|
||||
rewardsFiat = "$2.00",
|
||||
rewardBlockType = RewardBlockType.Rewards,
|
||||
rewardConstraints = null,
|
||||
),
|
||||
isActionable = true,
|
||||
balances = persistentListOf(),
|
||||
)
|
||||
val initialInfoState = mockk<StakingStates.InitialInfoState.Data>(relaxed = true) {
|
||||
every { this@mockk.yieldBalance } returns yieldBalance
|
||||
}
|
||||
val rewardsValidatorsState = mockk<StakingStates.RewardsValidatorsState.Data>(relaxed = true) {
|
||||
every { rewards } returns persistentListOf(reward1, reward2)
|
||||
}
|
||||
val uiState = mockk<StakingUiState>(relaxed = true) {
|
||||
every { this@mockk.initialInfoState } returns initialInfoState
|
||||
every { this@mockk.rewardsValidatorsState } returns rewardsValidatorsState
|
||||
}
|
||||
every { stateController.value } returns uiState
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.openRewardsValidators()
|
||||
|
||||
verify { analyticsEventHandler.send(match { it is StakingAnalyticsEvent.ButtonRewards }) }
|
||||
verify {
|
||||
analyticsEventHandler.send(
|
||||
event = StakingAnalyticsEvent.ButtonValidator(source = StakeScreenSource.Info)
|
||||
)
|
||||
}
|
||||
verify {
|
||||
stateController.update(any<(StakingUiState) -> StakingUiState>())
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN RewardsRequirementsError AND minimumAmount WHEN openRewardsValidators THEN alert shown call`() =
|
||||
runTest {
|
||||
every { messageSender.send(any()) } just Runs
|
||||
|
||||
val constraints = PendingActionConstraints(
|
||||
type = StakingActionType.CLAIM_REWARDS,
|
||||
amountArg = PendingAction.PendingActionArgs.Amount(
|
||||
required = true,
|
||||
minimum = BigDecimal.TEN,
|
||||
maximum = null,
|
||||
),
|
||||
)
|
||||
val yieldBalance = InnerYieldBalanceState.Data(
|
||||
integrationId = "test-integration",
|
||||
reward = YieldReward(
|
||||
rewardsCrypto = "1.0",
|
||||
rewardsFiat = "$1.00",
|
||||
rewardBlockType = RewardBlockType.RewardsRequirementsError,
|
||||
rewardConstraints = constraints,
|
||||
),
|
||||
isActionable = true,
|
||||
balances = persistentListOf(),
|
||||
)
|
||||
val initialInfoState = mockk<StakingStates.InitialInfoState.Data>(relaxed = true) {
|
||||
every { this@mockk.yieldBalance } returns yieldBalance
|
||||
}
|
||||
val uiState = mockk<StakingUiState>(relaxed = true) {
|
||||
every { this@mockk.initialInfoState } returns initialInfoState
|
||||
}
|
||||
every { stateController.value } returns uiState
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.openRewardsValidators()
|
||||
|
||||
verify { analyticsEventHandler.send(match { it is StakingAnalyticsEvent.ButtonRewards }) }
|
||||
verify { messageSender.send(any()) }
|
||||
verify(exactly = 0) { getActionRequirementAmountUseCase.invoke(any(), any()) }
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN RewardsRequirementsError WHEN openRewardsValidators THEN getActionRequirementAmountUseCase called`() =
|
||||
runTest {
|
||||
every { messageSender.send(any()) } just Runs
|
||||
every {
|
||||
getActionRequirementAmountUseCase.invoke(any(), any())
|
||||
} returns BigDecimal.ONE
|
||||
|
||||
val yieldBalance = InnerYieldBalanceState.Data(
|
||||
integrationId = "test-integration",
|
||||
reward = YieldReward(
|
||||
rewardsCrypto = "1.0",
|
||||
rewardsFiat = "$1.00",
|
||||
rewardBlockType = RewardBlockType.RewardsRequirementsError,
|
||||
rewardConstraints = null,
|
||||
),
|
||||
isActionable = true,
|
||||
balances = persistentListOf(),
|
||||
)
|
||||
val initialInfoState = mockk<StakingStates.InitialInfoState.Data>(relaxed = true) {
|
||||
every { this@mockk.yieldBalance } returns yieldBalance
|
||||
}
|
||||
val uiState = mockk<StakingUiState>(relaxed = true) {
|
||||
every { this@mockk.initialInfoState } returns initialInfoState
|
||||
}
|
||||
every { stateController.value } returns uiState
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.openRewardsValidators()
|
||||
|
||||
verify { analyticsEventHandler.send(match { it is StakingAnalyticsEvent.ButtonRewards }) }
|
||||
verify {
|
||||
getActionRequirementAmountUseCase.invoke(
|
||||
integrationId = "test-integration",
|
||||
actionType = StakingActionType.CLAIM_REWARDS
|
||||
)
|
||||
}
|
||||
verify { messageSender.send(any()) }
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN single pending action WHEN onActiveStake THEN prepareForConfirmation and onNextClick called`() =
|
||||
runTest {
|
||||
every { stateController.value } returns initialUiState
|
||||
|
||||
val singleAction = PendingAction(
|
||||
type = StakingActionType.CLAIM_REWARDS,
|
||||
passthrough = "test",
|
||||
args = null,
|
||||
)
|
||||
val activeStake: BalanceState = mockk(relaxed = true) {
|
||||
every { type } returns BalanceType.STAKED
|
||||
every { pendingActions } returns persistentListOf(singleAction)
|
||||
every { target } returns null
|
||||
every { cryptoValue } returns "100"
|
||||
}
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onActiveStake(activeStake)
|
||||
advanceUntilIdle()
|
||||
|
||||
// prepareForConfirmation calls updateAll with 4 transformers
|
||||
verify {
|
||||
stateController.updateAll(
|
||||
match { it is SetConfirmationStateInitTransformer },
|
||||
match { it is ValidatorSelectChangeTransformer },
|
||||
match { it is SetAmountDataTransformer },
|
||||
any(),
|
||||
)
|
||||
}
|
||||
// onNextClick updates step
|
||||
verify { stateController.update(any<(StakingUiState) -> StakingUiState>()) }
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN multiple pending actions WHEN onActiveStake THEN ShowActionSelectorBottomSheetTransformer applied`() =
|
||||
runTest {
|
||||
every { stateController.value } returns initialUiState
|
||||
|
||||
val action1 = PendingAction(
|
||||
type = StakingActionType.CLAIM_REWARDS,
|
||||
passthrough = "test1",
|
||||
args = null,
|
||||
)
|
||||
val action2 = PendingAction(
|
||||
type = StakingActionType.WITHDRAW,
|
||||
passthrough = "test2",
|
||||
args = null,
|
||||
)
|
||||
val activeStake: BalanceState = mockk(relaxed = true) {
|
||||
every { type } returns BalanceType.STAKED
|
||||
every { pendingActions } returns persistentListOf(action1, action2)
|
||||
every { target } returns null
|
||||
every { cryptoValue } returns "100"
|
||||
}
|
||||
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onActiveStake(activeStake)
|
||||
|
||||
verify {
|
||||
stateController.update(
|
||||
match<Transformer<StakingUiState>> { it is ShowActionSelectorBottomSheetTransformer },
|
||||
)
|
||||
}
|
||||
// prepareForConfirmation should NOT have been called
|
||||
verify(exactly = 0) {
|
||||
stateController.updateAll(
|
||||
match { it is SetConfirmationStateInitTransformer },
|
||||
any(), any(), any(),
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN onActiveStakeAnalytic THEN ButtonValidator analytics sent`() = runTest {
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onActiveStakeAnalytic()
|
||||
|
||||
verify {
|
||||
analyticsEventHandler.send(
|
||||
StakingAnalyticsEvent.ButtonValidator(
|
||||
source = StakeScreenSource.Info,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue