Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-04 12:50:14 +05:00
parent d0bc6087fe
commit a99a1b370a
15 changed files with 3076 additions and 60 deletions

View file

@ -4,6 +4,7 @@ import com.tangem.common.ui.alerts.TransactionErrorDialogFactory
import com.tangem.core.ui.message.DialogMessage
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.feature.swap.domain.models.ui.SwapTransactionState
import com.tangem.feature.swap.model.toExpressError
import com.tangem.feature.swap.models.SwapAlertUM
import com.tangem.feature.swap.utils.getExpressErrorMessage
import com.tangem.utils.converter.Converter
@ -17,14 +18,14 @@ internal class SwapTransactionErrorStateConverter(
return when (value) {
is SwapTransactionState.Error.TransactionError -> {
when (val error = value.error) {
is SendTransactionError.UserCancelledError -> return null
is SendTransactionError.UserCancelledError -> null
null -> SwapAlertUM.genericError(onDismiss)
else -> transactionErrorDialogFactory.create(error, onDismiss, onSupportClick)
}
}
is SwapTransactionState.Error.ExpressError -> {
SwapAlertUM.expressErrorAlert(
message = getExpressErrorMessage(value.error),
message = getExpressErrorMessage(value.error.toExpressError()),
onConfirmClick = { onSupportClick(value.error.code.toString()) },
)
}

View file

@ -222,6 +222,7 @@ internal class SwapModel @Inject constructor(
private val fromTokenBalanceJobHolder = JobHolder()
private val toTokenBalanceJobHolder = JobHolder()
private val swapPairsJobHolder = JobHolder()
private var isAmountChangedByUser: Boolean = false
private var lastPermissionNotificationTokens: Pair<String, String>? = null
@ -527,6 +528,11 @@ internal class SwapModel @Inject constructor(
private fun initSwapPairs(fromSwapCurrencyStatus: SwapCurrencyStatus, toSwapCurrencyStatus: SwapCurrencyStatus) {
modelScope.launch {
uiState = stateBuilder.createInitialLoadingState(
uiStateHolder = uiState,
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
toSwapCurrencyStatus = toSwapCurrencyStatus,
)
swapInteractor.getPair(
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
toSwapCurrencyStatus = toSwapCurrencyStatus,
@ -537,9 +543,11 @@ internal class SwapModel @Inject constructor(
},
).fold(
ifLeft = { error ->
handleSwapNotSupported(
uiState = stateBuilder.createInitialErrorState(
uiStateHolder = uiState,
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
toSwapCurrencyStatus = toSwapCurrencyStatus,
expressError = error,
onRetry = { retrySwapPairs(fromSwapCurrencyStatus, toSwapCurrencyStatus) },
)
TangemLogger.e("Error getting swap pair", error)
},
@ -555,6 +563,22 @@ internal class SwapModel @Inject constructor(
toSwapCurrencyStatus = toSwapCurrencyStatus,
)
} else {
uiState = stateBuilder.updateCurrenciesState(
uiStateHolder = uiState,
emptyAmountState = SwapState.EmptyAmountState(
zeroAmountEquivalent = stringReference(
BigDecimal.ZERO.format {
fiat(
fiatCurrencyCode = selectedAppCurrencyFlow.value.code,
fiatCurrencySymbol = selectedAppCurrencyFlow.value.symbol,
)
},
),
),
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
toSwapCurrencyStatus = toSwapCurrencyStatus,
shouldResetAmount = false,
)
dataState = dataState.copy(
pairs = pairs,
selectedPairProviders = providerList,
@ -569,7 +593,12 @@ internal class SwapModel @Inject constructor(
}
},
)
}
}.saveIn(swapPairsJobHolder)
}
private fun retrySwapPairs(fromSwapCurrencyStatus: SwapCurrencyStatus, toSwapCurrencyStatus: SwapCurrencyStatus) {
if (swapPairsJobHolder.isActive) return
initSwapPairs(fromSwapCurrencyStatus, toSwapCurrencyStatus)
}
@Suppress("UnusedPrivateMember")

View file

@ -10,6 +10,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork
@ -49,9 +50,15 @@ internal class SwapNotificationsFactory(
)
}
fun getNotAvailableStateNotifications(fromCurrencyName: String): ImmutableList<NotificationUM> {
fun getErrorStateNotification(
expressError: ExpressError,
onRetryClick: () -> Unit,
): ImmutableList<NotificationUM> {
return persistentListOf(
SwapNotificationUM.Warning.NoAvailableTokensToSwap(fromCurrencyName),
SwapNotificationUM.Warning.ExpressErrorWarning(
expressError = expressError,
onConfirmClick = onRetryClick,
),
)
}
@ -363,14 +370,55 @@ internal class SwapNotificationsFactory(
crypto(fromToken.symbol, fromToken.decimals)
},
)
else -> SwapNotificationUM.Warning.ExpressError(
expressDataError,
onConfirmClick = onRetryClick,
)
else -> {
val expressError = expressDataError.toExpressError()
SwapNotificationUM.Warning.ExpressErrorWarning(
expressError = expressError,
onConfirmClick = onRetryClick,
)
}
}
}
private fun needShowNetworkFeeCoverageWarningShow(quoteModel: SwapState.QuotesLoadedState): Boolean {
return quoteModel.currencyCheck?.existentialDeposit == null
}
}
@Deprecated("Remove with ExpressDataError")
@Suppress("CyclomaticComplexMethod")
internal fun ExpressDataError.toExpressError(): ExpressError = when (this) {
is ExpressDataError.BadRequest -> ExpressError.BadRequest(code)
is ExpressDataError.SwapsAreUnavailableNowError -> ExpressError.Forbidden(code)
is ExpressDataError.ExchangeProviderNotFoundError -> ExpressError.ProviderNotFoundError(code)
is ExpressDataError.ExchangeProviderNotActiveError -> ExpressError.ProviderNotActiveError(code)
is ExpressDataError.ExchangeProviderNotAvailableError -> ExpressError.ProviderNotAvailableError(code)
is ExpressDataError.ExchangeProviderProviderInternalError -> ExpressError.ProviderInternalError(code)
is ExpressDataError.ExchangeNotPossibleError -> ExpressError.ExchangeNotPossibleError(code)
is ExpressDataError.ExchangeNotEnoughBalanceError -> ExpressError.NotEnoughBalanceError(code)
is ExpressDataError.ExchangeInvalidAddressError -> ExpressError.InvalidAddressError(code)
is ExpressDataError.ExchangeTooSmallAmountError -> ExpressError.AmountError.TooSmallError(code, amount.value)
is ExpressDataError.ExchangeTooBigAmountError -> ExpressError.AmountError.TooBigError(code, amount.value)
is ExpressDataError.ExchangeNotEnoughAllowanceError -> ExpressError.AmountError.NotEnoughAllowanceError(
code = code,
amount = currentAllowance,
)
is ExpressDataError.ExchangeInvalidFromDecimalsError -> ExpressError.InvalidFromDecimalsError(
code = code,
receivedFromDecimals = receivedFromDecimals,
expressFromDecimals = expressFromDecimals,
)
is ExpressDataError.ProviderDifferentAmountError -> ExpressError.ProviderDifferentAmountError(
code = code,
fromAmount = fromAmount,
fromProviderAmount = fromProviderAmount,
decimals = decimals,
)
is ExpressDataError.InvalidSignatureError -> ExpressError.InvalidSignatureError(code)
is ExpressDataError.InvalidRequestIdError -> ExpressError.InvalidRequestIdError(code)
is ExpressDataError.InvalidPayoutAddressError -> ExpressError.InvalidPayoutAddressError(code)
is ExpressDataError.UnknownErrorWithCode -> ExpressError.InternalError(code)
ExpressDataError.UnknownError -> ExpressError.UnknownError
ExpressDataError.TooLargeSolanaTransactionError -> ExpressError.TooLargeSolanaTransactionError()
ExpressDataError.DexActiveSupplyError -> ExpressError.DexActiveSupplyError()
}

View file

@ -86,6 +86,7 @@ sealed interface TransactionCardType {
val onFocusChanged: ((Boolean) -> Unit),
override val inputError: InputError,
override val accountTitleUM: AccountTitleUM,
val isEnabled: Boolean,
) : TransactionCardType
data class ReadOnly(

View file

@ -1,14 +1,14 @@
package com.tangem.feature.swap.models.states
import com.tangem.common.ui.R
import com.tangem.common.ui.extensions.networkIconResId
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.common.ui.extensions.networkIconResId
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.feature.swap.utils.getExpressErrorMessage
import com.tangem.feature.swap.utils.getExpressErrorTitle
@ -171,12 +171,12 @@ internal object SwapNotificationUM {
),
)
data class ExpressError(
val expressDataError: ExpressDataError,
data class ExpressErrorWarning(
val expressError: ExpressError,
val onConfirmClick: () -> Unit,
) : Warning(
title = getExpressErrorTitle(expressDataError),
subtitle = getExpressErrorMessage(expressDataError),
title = getExpressErrorTitle(expressError),
subtitle = getExpressErrorMessage(expressError),
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.warning_button_refresh),
onClick = onConfirmClick,

View file

@ -10,6 +10,7 @@ import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
@ -31,12 +32,19 @@ import com.tangem.core.ui.res.TangemTheme
internal fun AutoSizeTextField(
textFieldValue: TextFieldValue,
focusRequester: FocusRequester,
isEnabled: Boolean,
onAmountChange: (String) -> Unit,
onFocusChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
val focusManager = LocalFocusManager.current
LaunchedEffect(isEnabled) {
if (!isEnabled) {
focusManager.clearFocus()
}
}
BoxWithConstraints(modifier = modifier.fillMaxWidth()) {
var shrunkFontSize = TangemTheme.typography.h2.fontSize
val calculateIntrinsics = @Composable {
@ -77,6 +85,7 @@ internal fun AutoSizeTextField(
imeAction = ImeAction.Done,
keyboardType = KeyboardType.Decimal,
),
enabled = isEnabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
decorationBox = { innerTextField ->
if (textFieldValue.text.isBlank()) {

View file

@ -14,7 +14,9 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.parseBigDecimalOrNull
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -38,6 +40,7 @@ import com.tangem.utils.Provider
import com.tangem.utils.StringsSigns
import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.utils.StringsSigns.TILDE_SIGN
import com.tangem.utils.extensions.orZero
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@ -106,11 +109,13 @@ internal class StateBuilder(
swapCurrencyStatus = fromSwapCurrencyStatus,
emptyAmountState = emptyAmountState,
isFromCard = true,
isEnabled = toSwapCurrencyStatus != null,
),
receiveCardData = createCardState(
swapCurrencyStatus = toSwapCurrencyStatus,
emptyAmountState = emptyAmountState,
isFromCard = false,
isEnabled = true,
),
notifications = persistentListOf(),
isInsufficientFunds = false,
@ -128,6 +133,78 @@ internal class StateBuilder(
)
}
fun createInitialErrorState(
fromSwapCurrencyStatus: SwapCurrencyStatus?,
uiStateHolder: SwapStateHolder,
expressError: ExpressError,
onRetry: () -> Unit,
): SwapStateHolder {
return uiStateHolder.copy(
sendCardData = (uiStateHolder.sendCardData as? SwapCardState.SwapCardData)?.copy(
type = (uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable)?.copy(
isEnabled = false,
) ?: uiStateHolder.sendCardData.type,
) ?: uiStateHolder.sendCardData,
notifications = notificationsFactory.getErrorStateNotification(
expressError = expressError,
onRetryClick = onRetry,
),
permissionUM = SwapPermissionUM.Empty,
fee = FeeItemState.Empty,
swapButton = fromSwapCurrencyStatus?.let {
SwapButton(
walletInteractionIcon = walletInterationIcon(fromSwapCurrencyStatus.userWallet),
isEnabled = false,
isHoldToConfirm = fromSwapCurrencyStatus.userWallet.isHotWallet,
onClick = actions.onSwapClick,
)
} ?: uiStateHolder.swapButton,
changeCardsButtonState = ChangeCardsButtonState.ENABLED,
providerState = ProviderState.Empty(),
priceImpact = PriceImpact.Empty,
tosState = null,
)
}
fun createInitialLoadingState(
fromSwapCurrencyStatus: SwapCurrencyStatus,
toSwapCurrencyStatus: SwapCurrencyStatus,
uiStateHolder: SwapStateHolder,
): SwapStateHolder {
val fromCurrency = fromSwapCurrencyStatus.currency
val toCurrency = toSwapCurrencyStatus.currency
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
return uiStateHolder.copy(
sendCardData = uiStateHolder.sendCardData.copy(
type = TransactionCardType.Inputtable(
onAmountChanged = actions.onAmountChanged,
onFocusChanged = actions.onAmountSelected,
inputError = TransactionCardType.InputError.Empty,
accountTitleUM = getCardAccountTitle(fromSwapCurrencyStatus.account, isFromCard = true),
isEnabled = true,
),
),
receiveCardData = uiStateHolder.receiveCardData.copy(
type = TransactionCardType.ReadOnly(
accountTitleUM = getCardAccountTitle(toSwapCurrencyStatus.account, isFromCard = false),
),
),
notifications = persistentListOf(),
fee = FeeItemState.Empty,
swapButton = SwapButton(
walletInteractionIcon = walletInterationIcon(fromSwapCurrencyStatus.userWallet),
isEnabled = false,
isHoldToConfirm = fromSwapCurrencyStatus.userWallet.isHotWallet,
onClick = {},
),
providerState = ProviderState.Empty(),
changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS,
priceImpact = PriceImpact.Empty,
shouldShowMaxAmount = shouldShowMaxAmount(fromCurrency, toCurrency),
)
}
fun updateCurrenciesState(
uiStateHolder: SwapStateHolder,
emptyAmountState: SwapState.EmptyAmountState,
@ -141,12 +218,14 @@ internal class StateBuilder(
emptyAmountState = emptyAmountState,
isFromCard = true,
shouldResetAmount = shouldResetAmount,
isEnabled = toSwapCurrencyStatus != null,
),
receiveCardData = uiStateHolder.receiveCardData.updateCurrencyStatus(
swapCurrencyStatus = toSwapCurrencyStatus,
emptyAmountState = emptyAmountState,
isFromCard = false,
shouldResetAmount = shouldResetAmount,
isEnabled = true,
),
notifications = persistentListOf(),
isInsufficientFunds = false,
@ -169,6 +248,7 @@ internal class StateBuilder(
emptyAmountState: SwapState.EmptyAmountState,
shouldResetAmount: Boolean,
isFromCard: Boolean,
isEnabled: Boolean,
): SwapCardState {
val cardType = if (isFromCard) {
TransactionCardType.Inputtable(
@ -176,6 +256,7 @@ internal class StateBuilder(
onFocusChanged = actions.onAmountSelected,
inputError = TransactionCardType.InputError.Empty,
accountTitleUM = getCardAccountTitle(swapCurrencyStatus?.account, true),
isEnabled = isEnabled,
)
} else {
TransactionCardType.ReadOnly(
@ -188,6 +269,7 @@ internal class StateBuilder(
swapCurrencyStatus = swapCurrencyStatus,
emptyAmountState = emptyAmountState,
isFromCard = isFromCard,
isEnabled = isEnabled,
)
} else if (shouldResetAmount) {
copy(
@ -218,6 +300,7 @@ internal class StateBuilder(
swapCurrencyStatus: SwapCurrencyStatus?,
emptyAmountState: SwapState.EmptyAmountState,
isFromCard: Boolean,
isEnabled: Boolean,
): SwapCardState {
return if (swapCurrencyStatus == null) {
getEmptyCardState(isFromCard = isFromCard, emptyAmountState = emptyAmountState)
@ -229,6 +312,7 @@ internal class StateBuilder(
onFocusChanged = actions.onAmountSelected,
inputError = TransactionCardType.InputError.Empty,
accountTitleUM = getCardAccountTitle(swapCurrencyStatus.account, true),
isEnabled = isEnabled,
)
} else {
TransactionCardType.ReadOnly(
@ -328,6 +412,7 @@ internal class StateBuilder(
onFocusChanged = actions.onAmountSelected,
inputError = TransactionCardType.InputError.Empty,
accountTitleUM = getCardAccountTitle(fromSwapCurrencyStatus.account, isFromCard = true),
isEnabled = true,
),
),
receiveCardData = uiStateHolder.receiveCardData.copy(
@ -509,7 +594,7 @@ internal class StateBuilder(
private fun getSwapButtonEnabled(notifications: ImmutableList<NotificationUM>, priceImpact: PriceImpact): Boolean {
return notifications.none { notification ->
notification is SwapNotificationUM.Error || notification is NotificationUM.Error ||
notification is SwapNotificationUM.Warning.ExpressError ||
notification is SwapNotificationUM.Warning.ExpressErrorWarning ||
notification is SwapNotificationUM.Warning.ExpressGeneralError ||
notification is SwapNotificationUM.Warning.NoAvailableTokensToSwap ||
notification is SwapNotificationUM.Warning.SwapNotSupported ||
@ -686,7 +771,7 @@ internal class StateBuilder(
minTxAmount: BigDecimal?,
): SwapStateHolder {
if (uiState.sendCardData !is SwapCardState.SwapCardData) return uiState
val amountToSend = amountRaw.toBigDecimalOrNull()
val amountToSend = amountRaw.parseBigDecimalOrNull()
val sendInput = if (minTxAmount != null && amountToSend != null && amountToSend < minTxAmount) {
val minAmountFormatted = minTxAmount.format {
crypto(cryptoCurrency = fromSwapCurrencyStatus.currency, ignoreSymbolPosition = true)
@ -711,7 +796,7 @@ internal class StateBuilder(
),
amountEquivalent = getFormattedFiatAmount(
fromSwapCurrencyStatus.status.value.fiatRate?.let { fiatRate ->
amountToSend?.multiply(fiatRate)
amountToSend?.multiply(fiatRate).orZero()
},
),
type = sendInput,
@ -731,12 +816,14 @@ internal class StateBuilder(
emptyAmountState = emptyAmountState,
isFromCard = true,
shouldResetAmount = false,
isEnabled = toSwapCurrencyStatus != null,
),
receiveCardData = uiState.receiveCardData.updateCurrencyStatus(
swapCurrencyStatus = toSwapCurrencyStatus,
emptyAmountState = emptyAmountState,
isFromCard = false,
shouldResetAmount = false,
isEnabled = true,
),
)
}

View file

@ -296,25 +296,16 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie
} else {
TangemTheme.colors.text.warning
}
AnimatedContent(
targetState = type.accountTitleUM,
label = "",
) { currentAccountTitle ->
if (currentAccountTitle != null) {
AccountTitle(
accountTitleUM = currentAccountTitle,
textColor = titleColor,
)
} else {
TextShimmer(
text = stringResourceSafe(R.string.swapping_to_title),
style = TangemTheme.typography.subtitle2,
)
}
}
AccountTitle(
accountTitleUM = type.accountTitleUM,
textColor = titleColor,
)
SpacerW16()
if (balance.isNotBlank()) {
AnimatedContent(targetState = balance, label = "") { balanceText ->
AnimatedContent(
targetState = balance,
label = "",
) { balanceText ->
Text(
text = balanceText,
color = TangemTheme.colors.text.tertiary,
@ -391,6 +382,7 @@ private fun Content(
modifier = sumTextModifier.testTag(SwapTokenScreenTestTags.SWAP_TEXT_FIELD),
focusRequester = focusRequester,
textFieldValue = textFieldValue ?: TextFieldValue(),
isEnabled = type.isEnabled,
onAmountChange = { type.onAmountChanged(it) },
onFocusChange = type.onFocusChanged,
)

View file

@ -24,12 +24,13 @@ internal object SwapTransactionCardPreview {
name = AccountNameUM.DefaultMain.value,
icon = CryptoPortfolioIconConverter.convert(CryptoPortfolioIcon.ofDefaultCustomAccount()),
),
isEnabled = true,
),
amountTextFieldValue = TextFieldValue(),
amountEquivalent = stringReference("1 000 000"),
currencyIconState = CurrencyIconState.Loading,
tokenSymbol = stringReference("DAI"),
balance = "123",
balance = "123123123.123123",
isBalanceHidden = false,
)
@ -63,6 +64,7 @@ internal object SwapTransactionCardPreview {
onFocusChanged = {},
inputError = TransactionCardType.InputError.Empty,
accountTitleUM = AccountTitleUM.Text(title = resourceReference(R.string.swapping_from_title)),
isEnabled = false,
),
amountEquivalent = stringReference("$0.00"),
amountTextFieldValue = null,
@ -74,6 +76,7 @@ internal object SwapTransactionCardPreview {
onFocusChanged = {},
inputError = TransactionCardType.InputError.Empty,
accountTitleUM = AccountTitleUM.Text(title = resourceReference(R.string.swapping_to_title)),
isEnabled = false,
),
)
}

View file

@ -5,48 +5,50 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.simple
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.presentation.R
internal fun getExpressErrorMessage(expressDataError: ExpressDataError): TextReference {
return when (expressDataError) {
is ExpressDataError.SwapsAreUnavailableNowError -> resourceReference(
internal fun getExpressErrorMessage(expressError: ExpressError): TextReference {
return when (expressError) {
is ExpressError.InternalError,
is ExpressError.Forbidden,
-> resourceReference(
id = R.string.express_error_swap_unavailable,
formatArgs = wrappedList(expressDataError.code),
formatArgs = wrappedList(expressError.code),
)
is ExpressDataError.ExchangeNotPossibleError -> resourceReference(
is ExpressError.ExchangeNotPossibleError -> resourceReference(
id = R.string.warning_express_pair_unavailable_message,
formatArgs = wrappedList(expressDataError.code),
formatArgs = wrappedList(expressError.code),
)
is ExpressDataError.UnknownError -> resourceReference(R.string.common_unknown_error)
is ExpressDataError.ExchangeProviderNotActiveError,
is ExpressDataError.ExchangeProviderNotFoundError,
is ExpressDataError.ExchangeProviderNotAvailableError,
is ExpressDataError.ExchangeProviderProviderInternalError,
is ExpressError.UnknownError -> resourceReference(R.string.common_unknown_error)
is ExpressError.ProviderNotActiveError,
is ExpressError.ProviderNotFoundError,
is ExpressError.ProviderNotAvailableError,
is ExpressError.ProviderInternalError,
-> resourceReference(
id = R.string.express_error_swap_pair_unavailable,
formatArgs = wrappedList(expressDataError.code),
formatArgs = wrappedList(expressError.code),
)
is ExpressDataError.ProviderDifferentAmountError -> resourceReference(
is ExpressError.ProviderDifferentAmountError -> resourceReference(
R.string.express_error_provider_amount_roundup,
formatArgs = wrappedList(
expressDataError.code,
expressDataError.fromProviderAmount.format { simple(decimals = expressDataError.decimals) },
expressError.code,
expressError.fromProviderAmount.format { simple(decimals = expressError.decimals) },
),
)
else -> resourceReference(R.string.express_error_code, wrappedList(expressDataError.code.toString()))
else -> resourceReference(R.string.express_error_code, wrappedList(expressError.code.toString()))
}
}
internal fun getExpressErrorTitle(expressDataError: ExpressDataError): TextReference {
return when (expressDataError) {
is ExpressDataError.ExchangeNotPossibleError -> resourceReference(
internal fun getExpressErrorTitle(expressError: ExpressError): TextReference {
return when (expressError) {
is ExpressError.ExchangeNotPossibleError -> resourceReference(
id = R.string.warning_express_pair_unavailable_title,
formatArgs = wrappedList(expressDataError.code),
formatArgs = wrappedList(expressError.code),
)
is ExpressDataError.UnknownError -> resourceReference(R.string.common_error)
is ExpressError.UnknownError -> resourceReference(R.string.common_error)
else -> resourceReference(R.string.warning_express_refresh_required_title)
}
}

View file

@ -231,6 +231,115 @@ internal class DefaultInitialCurrenciesResolverTest {
assertThat(to).isNull()
}
@Test
fun `GIVEN Token on ETH network with higher fiat vs Coin on BTC network with lower fiat WHEN no initial currency THEN Token is selected as FROM`() =
runTest {
val ethTokenId = mockCurrencyId(rawNetworkId = "ethereum", contractAddress = "0xUSDT")
val btcCoinId = mockCurrencyId(rawNetworkId = "bitcoin", contractAddress = "")
val ethToken = mockk<CryptoCurrency.Token>(relaxed = true) {
every { id } returns ethTokenId
}
val btcCoin = mockk<CryptoCurrency.Coin>(relaxed = true) {
every { id } returns btcCoinId
}
val tokenStatus = createCurrencyStatus(ethToken, fiatAmount = BigDecimal("500"))
val coinStatus = createCurrencyStatus(btcCoin, fiatAmount = BigDecimal("100"))
// currencies list order must match the order passed to setupAvailability
val accountStatus = createCryptoPortfolioAccountStatus(listOf(coinStatus, tokenStatus))
setupSupplier(listOf(accountStatus))
setupAvailability(linkedMapOf(btcCoin to true, ethToken to true))
val (from, to) = resolver.invoke(
userWalletId,
initialCryptoCurrency = null,
swapCurrencyPosition = CurrencyPosition.ANY,
isPaymentAccount = false,
)
assertThat(from?.status).isSameInstanceAs(tokenStatus)
assertThat(to).isNull()
}
@Test
fun `GIVEN Token on ETH with highest fiat vs Coin on BTC with mid fiat vs Coin on SOL with lowest fiat WHEN no initial currency THEN Token is selected as FROM`() =
runTest {
val usdtId = mockCurrencyId(rawNetworkId = "ethereum", contractAddress = "0xUSDT")
val btcId = mockCurrencyId(rawNetworkId = "bitcoin", contractAddress = "")
val solId = mockCurrencyId(rawNetworkId = "solana", contractAddress = "")
val usdtToken = mockk<CryptoCurrency.Token>(relaxed = true) {
every { id } returns usdtId
}
val btcCoin = mockk<CryptoCurrency.Coin>(relaxed = true) {
every { id } returns btcId
}
val solCoin = mockk<CryptoCurrency.Coin>(relaxed = true) {
every { id } returns solId
}
val usdtStatus = createCurrencyStatus(usdtToken, fiatAmount = BigDecimal("1000"))
val btcStatus = createCurrencyStatus(btcCoin, fiatAmount = BigDecimal("500"))
val solStatus = createCurrencyStatus(solCoin, fiatAmount = BigDecimal("100"))
// list order: btcStatus, solStatus, usdtStatus → setupAvailability must match
val accountStatus = createCryptoPortfolioAccountStatus(listOf(btcStatus, solStatus, usdtStatus))
setupSupplier(listOf(accountStatus))
setupAvailability(linkedMapOf(btcCoin to true, solCoin to true, usdtToken to true))
val (from, to) = resolver.invoke(
userWalletId,
initialCryptoCurrency = null,
swapCurrencyPosition = CurrencyPosition.ANY,
isPaymentAccount = false,
)
assertThat(from?.status).isSameInstanceAs(usdtStatus)
assertThat(to).isNull()
}
@Test
fun `GIVEN Coin on BTC with highest fiat vs Token on ETH with mid fiat vs Token on SOL with lowest fiat WHEN no initial currency THEN Coin is selected as FROM`() =
runTest {
val btcId = mockCurrencyId(rawNetworkId = "bitcoin", contractAddress = "")
val usdtId = mockCurrencyId(rawNetworkId = "ethereum", contractAddress = "0xUSDT")
val usdcId = mockCurrencyId(rawNetworkId = "solana", contractAddress = "EPjFWdd5")
val btcCoin = mockk<CryptoCurrency.Coin>(relaxed = true) {
every { id } returns btcId
}
val usdtToken = mockk<CryptoCurrency.Token>(relaxed = true) {
every { id } returns usdtId
}
val usdcToken = mockk<CryptoCurrency.Token>(relaxed = true) {
every { id } returns usdcId
}
val btcStatus = createCurrencyStatus(btcCoin, fiatAmount = BigDecimal("2000"))
val usdtStatus = createCurrencyStatus(usdtToken, fiatAmount = BigDecimal("600"))
val usdcStatus = createCurrencyStatus(usdcToken, fiatAmount = BigDecimal("150"))
// list order: usdtStatus, usdcStatus, btcStatus → setupAvailability must match
val accountStatus = createCryptoPortfolioAccountStatus(listOf(usdtStatus, usdcStatus, btcStatus))
setupSupplier(listOf(accountStatus))
setupAvailability(linkedMapOf(usdtToken to true, usdcToken to true, btcCoin to true))
val (from, to) = resolver.invoke(
userWalletId,
initialCryptoCurrency = null,
swapCurrencyPosition = CurrencyPosition.ANY,
isPaymentAccount = false,
)
assertThat(from?.status).isSameInstanceAs(btcStatus)
assertThat(to).isNull()
}
// endregion
// region initial currency tests
@ -747,6 +856,294 @@ internal class DefaultInitialCurrenciesResolverTest {
// endregion
// region multi-account balance selection (no initial currency)
@Test
fun `GIVEN two accounts where secondary has higher balance WHEN no initial currency THEN picks token from secondary account`() =
runTest {
// Main account: currency with low balance.
val mainCurrency = mockCryptoCurrency()
val mainStatus = createCurrencyStatus(mainCurrency, fiatAmount = BigDecimal("10"))
val mainAccount = createCryptoPortfolioAccountStatus(
currencies = listOf(mainStatus),
derivationIndexValue = 0,
)
// Secondary account: currency with higher balance.
val secondaryCurrency = mockCryptoCurrency()
val secondaryStatus = createCurrencyStatus(secondaryCurrency, fiatAmount = BigDecimal("500"))
val secondaryAccount = createCryptoPortfolioAccountStatus(
currencies = listOf(secondaryStatus),
derivationIndexValue = 1,
)
setupSupplier(listOf(mainAccount, secondaryAccount))
setupAvailability(linkedMapOf(mainCurrency to true))
setupAvailability(linkedMapOf(secondaryCurrency to true))
val (from, to) = resolver.invoke(
userWalletId,
initialCryptoCurrency = null,
swapCurrencyPosition = CurrencyPosition.ANY,
isPaymentAccount = false,
)
// Global max-balance candidate lives in the secondary account.
assertThat(from?.status).isSameInstanceAs(secondaryStatus)
assertThat(to).isNull()
}
@Test
fun `GIVEN two accounts each with several currencies where highest fiat token is in account 2 WHEN no initial currency THEN that token wins`() =
runTest {
val c1 = mockCryptoCurrency()
val c2 = mockCryptoCurrency()
val c3 = mockCryptoCurrency()
val c4 = mockCryptoCurrency()
val s1 = createCurrencyStatus(c1, fiatAmount = BigDecimal("100"))
val s2 = createCurrencyStatus(c2, fiatAmount = BigDecimal("200"))
// c3 is a Token in account 2 with the highest fiat.
val s3 = createCurrencyStatus(c3, fiatAmount = BigDecimal("999"))
val s4 = createCurrencyStatus(c4, fiatAmount = BigDecimal("50"))
val account1 = createCryptoPortfolioAccountStatus(
currencies = listOf(s1, s2),
derivationIndexValue = 0,
)
val account2 = createCryptoPortfolioAccountStatus(
currencies = listOf(s4, s3),
derivationIndexValue = 1,
)
setupSupplier(listOf(account1, account2))
setupAvailability(linkedMapOf(c1 to true, c2 to true))
setupAvailability(linkedMapOf(c4 to true, c3 to true))
val (from, to) = resolver.invoke(
userWalletId,
initialCryptoCurrency = null,
swapCurrencyPosition = CurrencyPosition.ANY,
isPaymentAccount = false,
)
assertThat(from?.status).isSameInstanceAs(s3)
assertThat(to).isNull()
}
@Test
fun `GIVEN two accounts all balances are zero or null WHEN no initial currency THEN falls back to first currency of first account`() =
runTest {
val c1 = mockCryptoCurrency()
val c2 = mockCryptoCurrency()
val s1 = createCurrencyStatus(c1, fiatAmount = BigDecimal.ZERO)
val s2 = createCurrencyStatus(c2, fiatAmount = null)
val account1 = createCryptoPortfolioAccountStatus(
currencies = listOf(s1),
derivationIndexValue = 0,
)
val account2 = createCryptoPortfolioAccountStatus(
currencies = listOf(s2),
derivationIndexValue = 1,
)
setupSupplier(listOf(account1, account2))
setupAvailability(linkedMapOf(c1 to true))
setupAvailability(linkedMapOf(c2 to true))
val (from, to) = resolver.invoke(
userWalletId,
initialCryptoCurrency = null,
swapCurrencyPosition = CurrencyPosition.ANY,
isPaymentAccount = false,
)
// cryptoPortfolioAccountsMap.entries.firstOrNull()?.value?.firstOrNull() = s1.
assertThat(from?.status).isSameInstanceAs(s1)
assertThat(to).isNull()
}
@Test
fun `GIVEN first account is empty and second account has currencies with balance WHEN no initial currency THEN picks highest balance from second account`() =
runTest {
val c1 = mockCryptoCurrency()
val c2 = mockCryptoCurrency()
val s1 = createCurrencyStatus(c1, fiatAmount = BigDecimal("150"))
val s2 = createCurrencyStatus(c2, fiatAmount = BigDecimal("300"))
// account1 has no currencies at all.
val account1 = createCryptoPortfolioAccountStatus(
currencies = emptyList(),
derivationIndexValue = 0,
)
val account2 = createCryptoPortfolioAccountStatus(
currencies = listOf(s1, s2),
derivationIndexValue = 1,
)
setupSupplier(listOf(account1, account2))
// account1 is empty so rampStateManager is called with an empty list for it.
coEvery { rampStateManager.availableForSwap(userWalletId, emptyList()) } returns emptyMap()
setupAvailability(linkedMapOf(c1 to true, c2 to true))
val (from, to) = resolver.invoke(
userWalletId,
initialCryptoCurrency = null,
swapCurrencyPosition = CurrencyPosition.ANY,
isPaymentAccount = false,
)
assertThat(from?.status).isSameInstanceAs(s2)
assertThat(to).isNull()
}
// endregion
// region initial currency + CurrencyPosition.ANY going to TO, scoped to same account
@Test
fun `GIVEN selected zero-balance currency in account 1 goes to TO WHEN account 2 has higher-balance currency THEN FROM is picked only from account 1`() =
runTest {
// Account 1: the initial currency (available, zero balance → TO) + a companion.
val initialId = mockCurrencyId("ethereum", "0xDAI")
val initialCurrency = mockCryptoCurrency(id = initialId)
val initialInAccount1 = mockCryptoCurrency(id = initialId)
val companion1 = mockCryptoCurrency()
val initialStatus = createCurrencyStatus(initialInAccount1, fiatAmount = BigDecimal.ZERO)
val companionStatus = createCurrencyStatus(companion1, fiatAmount = BigDecimal("75"))
val account1 = createCryptoPortfolioAccountStatus(
currencies = listOf(initialStatus, companionStatus),
derivationIndexValue = 0,
)
// Account 2: has a currency with much higher balance that must NOT be chosen as FROM.
val highBalanceCurrency = mockCryptoCurrency()
val highBalanceStatus = createCurrencyStatus(highBalanceCurrency, fiatAmount = BigDecimal("9999"))
val account2 = createCryptoPortfolioAccountStatus(
currencies = listOf(highBalanceStatus),
derivationIndexValue = 1,
)
setupSupplier(listOf(account1, account2))
setupAvailability(linkedMapOf(initialInAccount1 to true, companion1 to true))
setupAvailability(linkedMapOf(highBalanceCurrency to true))
val (from, to) = resolver.invoke(
userWalletId,
initialCryptoCurrency = initialCurrency,
swapCurrencyPosition = CurrencyPosition.ANY,
isPaymentAccount = false,
)
// FROM must be from account1, never the high-balance token from account2.
assertThat(from?.status).isSameInstanceAs(companionStatus)
assertThat(to?.status).isSameInstanceAs(initialStatus)
}
@Test
fun `GIVEN selected zero-balance currency goes to TO WHEN same account has multiple currencies THEN FROM is the highest-balance one within same account`() =
runTest {
val initialId = mockCurrencyId("solana", "")
val initialCurrency = mockCryptoCurrency(id = initialId)
val initialInAccount = mockCryptoCurrency(id = initialId)
val lowBalance = mockCryptoCurrency()
val midBalance = mockCryptoCurrency()
val highBalance = mockCryptoCurrency()
val initialStatus = createCurrencyStatus(initialInAccount, fiatAmount = BigDecimal.ZERO)
val lowStatus = createCurrencyStatus(lowBalance, fiatAmount = BigDecimal("10"))
val midStatus = createCurrencyStatus(midBalance, fiatAmount = BigDecimal("100"))
val highStatus = createCurrencyStatus(highBalance, fiatAmount = BigDecimal("500"))
val account1 = createCryptoPortfolioAccountStatus(
currencies = listOf(initialStatus, lowStatus, midStatus, highStatus),
derivationIndexValue = 0,
)
// Account 2 has an even higher balance that must NOT be picked.
val outsiderCurrency = mockCryptoCurrency()
val outsiderStatus = createCurrencyStatus(outsiderCurrency, fiatAmount = BigDecimal("10000"))
val account2 = createCryptoPortfolioAccountStatus(
currencies = listOf(outsiderStatus),
derivationIndexValue = 1,
)
setupSupplier(listOf(account1, account2))
setupAvailability(linkedMapOf(initialInAccount to true, lowBalance to true, midBalance to true, highBalance to true))
setupAvailability(linkedMapOf(outsiderCurrency to true))
val (from, to) = resolver.invoke(
userWalletId,
initialCryptoCurrency = initialCurrency,
swapCurrencyPosition = CurrencyPosition.ANY,
isPaymentAccount = false,
)
// FROM must be the highest-balance token within account 1 only.
assertThat(from?.status).isSameInstanceAs(highStatus)
assertThat(to?.status).isSameInstanceAs(initialStatus)
}
// endregion
// region isSameTokenAs dedupe across accounts
@Test
fun `GIVEN same token in two accounts where selected is in account 1 WHEN going to TO THEN duplicate in account 2 is not considered and FROM is other account 1 currency`() =
runTest {
val sharedNetworkId = "polygon"
val sharedContract = "0xUSDC"
// Same token in two accounts (different IDs due to different derivations).
val idInAccount1 = mockCurrencyId(sharedNetworkId, sharedContract)
val idInAccount2 = mockCurrencyId(sharedNetworkId, sharedContract)
val initialCurrency = mockCryptoCurrency(id = idInAccount1)
val usdcInAccount1 = mockCryptoCurrency(id = idInAccount1)
val usdcInAccount2 = mockCryptoCurrency(id = idInAccount2)
// Account 1 also has a distinct companion currency.
val account1Companion = mockCryptoCurrency()
val initialStatus = createCurrencyStatus(usdcInAccount1, fiatAmount = BigDecimal.ZERO)
val companionStatus = createCurrencyStatus(account1Companion, fiatAmount = BigDecimal("200"))
val account1 = createCryptoPortfolioAccountStatus(
currencies = listOf(initialStatus, companionStatus),
derivationIndexValue = 0,
)
// Account 2 has the duplicate token with a large balance — must NOT be picked.
val duplicateStatus = createCurrencyStatus(usdcInAccount2, fiatAmount = BigDecimal("5000"))
val account2 = createCryptoPortfolioAccountStatus(
currencies = listOf(duplicateStatus),
derivationIndexValue = 1,
)
setupSupplier(listOf(account1, account2))
setupAvailability(linkedMapOf(usdcInAccount1 to true, account1Companion to true))
setupAvailability(linkedMapOf(usdcInAccount2 to true))
val (from, to) = resolver.invoke(
userWalletId,
initialCryptoCurrency = initialCurrency,
swapCurrencyPosition = CurrencyPosition.ANY,
isPaymentAccount = false,
)
// FROM must be the account1 companion (scoped to account1, duplicate in account2 excluded).
assertThat(from?.status).isSameInstanceAs(companionStatus)
assertThat(to?.status).isSameInstanceAs(initialStatus)
}
// endregion
// region helpers
private fun mockCryptoCurrency(

View file

@ -0,0 +1,592 @@
package com.tangem.feature.swap
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.models.account.Account
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.swap.models.SwapCurrencyStatus
import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork
import com.tangem.feature.swap.domain.models.ui.PriceImpact
import com.tangem.feature.swap.domain.models.ui.SwapState
import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.models.states.FeeItemState
import com.tangem.feature.swap.models.states.ProviderState
import com.tangem.feature.swap.models.states.SwapNotificationUM
import com.tangem.feature.swap.ui.StateBuilder
import com.tangem.utils.Provider
import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
internal class StateBuilderInitialStateTest {
private val actions: UiActions = mockk(relaxed = true)
private val isBalanceHiddenProvider: Provider<Boolean> = mockk()
private val appCurrencyProvider: Provider<AppCurrency> = mockk()
private val isAccountsModeProvider: Provider<Boolean> = mockk()
private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk()
private lateinit var sut: StateBuilder
private val appCurrency = AppCurrency.Default
@BeforeEach
fun setup() {
every { isBalanceHiddenProvider() } returns false
every { appCurrencyProvider() } returns appCurrency
every { isAccountsModeProvider() } returns false
sut = StateBuilder(
actions = actions,
isBalanceHiddenProvider = isBalanceHiddenProvider,
appCurrencyProvider = appCurrencyProvider,
isAccountsModeProvider = isAccountsModeProvider,
iGaslessFeeSupportedForNetwork = iGaslessFeeSupportedForNetwork,
)
}
// region createInitialLoadingState (no-arg overload)
@Nested
inner class `createInitialLoadingState no-arg` {
@Test
fun `should return loading state with disabled swap button`() {
val result = sut.createInitialLoadingState()
assertThat(result.swapButton.isEnabled).isFalse()
assertThat(result.swapButton.isInProgress).isTrue()
assertThat(result.swapButton.isHoldToConfirm).isFalse()
}
@Test
fun `should return loading state with empty send and receive cards`() {
val result = sut.createInitialLoadingState()
assertThat(result.sendCardData).isInstanceOf(SwapCardState.Empty::class.java)
assertThat(result.receiveCardData).isInstanceOf(SwapCardState.Empty::class.java)
}
@Test
fun `should return loading state with Empty fee`() {
val result = sut.createInitialLoadingState()
assertThat(result.fee).isInstanceOf(FeeItemState.Empty::class.java)
}
@Test
fun `should return loading state with DISABLED changeCardsButtonState`() {
val result = sut.createInitialLoadingState()
assertThat(result.changeCardsButtonState).isEqualTo(ChangeCardsButtonState.DISABLED)
}
@Test
fun `should return loading state with Empty providerState`() {
val result = sut.createInitialLoadingState()
assertThat(result.providerState).isInstanceOf(ProviderState.Empty::class.java)
}
@Test
fun `should return loading state with null walletInteractionIcon`() {
val result = sut.createInitialLoadingState()
assertThat(result.swapButton.walletInteractionIcon).isNull()
}
}
// endregion
// region createInitialReadyState
@Nested
inner class CreateInitialReadyState {
private val userWalletId = UserWalletId("aabbccdd")
private val userWallet: UserWallet.Cold = mockk(relaxed = true) {
every { walletId } returns userWalletId
}
private val emptyAmountState = SwapState.EmptyAmountState(
zeroAmountEquivalent = com.tangem.core.ui.extensions.stringReference("$0.00"),
)
private val baseState get() = sut.createInitialLoadingState()
@Test
fun `GIVEN both currencies non-null WHEN called THEN sendCard is SwapCardData`() {
val fromStatus = buildSwapCurrencyStatus(userWallet)
val toStatus = buildSwapCurrencyStatus(userWallet)
val result = sut.createInitialReadyState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
)
assertThat(result.sendCardData).isInstanceOf(SwapCardState.SwapCardData::class.java)
}
@Test
fun `GIVEN both currencies non-null WHEN called THEN receiveCard is SwapCardData`() {
val fromStatus = buildSwapCurrencyStatus(userWallet)
val toStatus = buildSwapCurrencyStatus(userWallet)
val result = sut.createInitialReadyState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
)
assertThat(result.receiveCardData).isInstanceOf(SwapCardState.SwapCardData::class.java)
}
@Test
fun `GIVEN fromCurrency is null WHEN called THEN sendCard is Empty`() {
val toStatus = buildSwapCurrencyStatus(userWallet)
val result = sut.createInitialReadyState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = null,
toSwapCurrencyStatus = toStatus,
)
assertThat(result.sendCardData).isInstanceOf(SwapCardState.Empty::class.java)
}
@Test
fun `GIVEN fromCurrency is null WHEN called THEN swapButton has no walletInteractionIcon`() {
val toStatus = buildSwapCurrencyStatus(userWallet)
val result = sut.createInitialReadyState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = null,
toSwapCurrencyStatus = toStatus,
)
assertThat(result.swapButton.walletInteractionIcon).isNull()
}
@Test
fun `GIVEN cold wallet WHEN called THEN swapButton isHoldToConfirm is false`() {
val fromStatus = buildSwapCurrencyStatus(userWallet)
val toStatus = buildSwapCurrencyStatus(userWallet)
val result = sut.createInitialReadyState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
)
assertThat(result.swapButton.isHoldToConfirm).isFalse()
}
@Test
fun `GIVEN hot wallet WHEN called THEN swapButton isHoldToConfirm is true`() {
val hotWallet: UserWallet.Hot = mockk(relaxed = true) {
every { walletId } returns userWalletId
}
val fromStatus = buildSwapCurrencyStatus(hotWallet)
val toStatus = buildSwapCurrencyStatus(hotWallet)
val result = sut.createInitialReadyState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
)
assertThat(result.swapButton.isHoldToConfirm).isTrue()
}
@Test
fun `WHEN called THEN changeCardsButtonState is ENABLED`() {
val result = sut.createInitialReadyState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = null,
toSwapCurrencyStatus = null,
)
assertThat(result.changeCardsButtonState).isEqualTo(ChangeCardsButtonState.ENABLED)
}
@Test
fun `WHEN called THEN swapButton is disabled`() {
val fromStatus = buildSwapCurrencyStatus(userWallet)
val toStatus = buildSwapCurrencyStatus(userWallet)
val result = sut.createInitialReadyState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
)
assertThat(result.swapButton.isEnabled).isFalse()
}
@Test
fun `WHEN called THEN providerState is Empty`() {
val result = sut.createInitialReadyState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = null,
toSwapCurrencyStatus = null,
)
assertThat(result.providerState).isInstanceOf(ProviderState.Empty::class.java)
}
@Test
fun `WHEN called THEN priceImpact is Empty`() {
val result = sut.createInitialReadyState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = null,
toSwapCurrencyStatus = null,
)
assertThat(result.priceImpact).isEqualTo(PriceImpact.Empty)
}
@Test
fun `WHEN called THEN notifications is empty`() {
val result = sut.createInitialReadyState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = null,
toSwapCurrencyStatus = null,
)
assertThat(result.notifications).isEmpty()
}
}
// endregion
// region createInitialErrorState
@Nested
inner class CreateInitialErrorState {
private val userWalletId = UserWalletId("aabbccdd")
private val coldWallet: UserWallet.Cold = mockk(relaxed = true) {
every { walletId } returns userWalletId
}
private val hotWallet: UserWallet.Hot = mockk(relaxed = true) {
every { walletId } returns userWalletId
}
private val expressError: ExpressError = ExpressError.UnknownError
private fun buildBaseStateWithSwapCardData(userWallet: UserWallet): SwapStateHolder {
val emptyAmountState = SwapState.EmptyAmountState(
zeroAmountEquivalent = com.tangem.core.ui.extensions.stringReference("$0.00"),
)
val fromStatus = buildSwapCurrencyStatus(userWallet)
val toStatus = buildSwapCurrencyStatus(userWallet)
val loading = sut.createInitialLoadingState()
return sut.createInitialReadyState(
uiStateHolder = loading,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
)
}
@Test
fun `GIVEN fromSwapCurrencyStatus is null WHEN called THEN swapButton comes from uiStateHolder`() {
val baseState = buildBaseStateWithSwapCardData(coldWallet)
val originalButton = baseState.swapButton
val result = sut.createInitialErrorState(
fromSwapCurrencyStatus = null,
uiStateHolder = baseState,
expressError = expressError,
onRetry = {},
)
assertThat(result.swapButton).isEqualTo(originalButton)
}
@Test
fun `GIVEN fromSwapCurrencyStatus non-null with cold wallet WHEN called THEN swapButton isHoldToConfirm is false`() {
val baseState = buildBaseStateWithSwapCardData(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.createInitialErrorState(
fromSwapCurrencyStatus = fromStatus,
uiStateHolder = baseState,
expressError = expressError,
onRetry = {},
)
assertThat(result.swapButton.isHoldToConfirm).isFalse()
}
@Test
fun `GIVEN fromSwapCurrencyStatus non-null with hot wallet WHEN called THEN swapButton isHoldToConfirm is true`() {
val baseState = buildBaseStateWithSwapCardData(hotWallet)
val fromStatus = buildSwapCurrencyStatus(hotWallet)
val result = sut.createInitialErrorState(
fromSwapCurrencyStatus = fromStatus,
uiStateHolder = baseState,
expressError = expressError,
onRetry = {},
)
assertThat(result.swapButton.isHoldToConfirm).isTrue()
}
@Test
fun `WHEN called THEN swapButton is disabled`() {
val baseState = buildBaseStateWithSwapCardData(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.createInitialErrorState(
fromSwapCurrencyStatus = fromStatus,
uiStateHolder = baseState,
expressError = expressError,
onRetry = {},
)
assertThat(result.swapButton.isEnabled).isFalse()
}
@Test
fun `WHEN called THEN notifications contains ExpressErrorWarning`() {
val baseState = buildBaseStateWithSwapCardData(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.createInitialErrorState(
fromSwapCurrencyStatus = fromStatus,
uiStateHolder = baseState,
expressError = expressError,
onRetry = {},
)
assertThat(result.notifications).hasSize(1)
assertThat(result.notifications[0]).isInstanceOf(SwapNotificationUM.Warning.ExpressErrorWarning::class.java)
}
@Test
fun `WHEN called THEN permissionUM is Empty`() {
val baseState = buildBaseStateWithSwapCardData(coldWallet)
val result = sut.createInitialErrorState(
fromSwapCurrencyStatus = null,
uiStateHolder = baseState,
expressError = expressError,
onRetry = {},
)
assertThat(result.permissionUM).isEqualTo(SwapPermissionUM.Empty)
}
@Test
fun `WHEN called THEN fee is Empty`() {
val baseState = buildBaseStateWithSwapCardData(coldWallet)
val result = sut.createInitialErrorState(
fromSwapCurrencyStatus = null,
uiStateHolder = baseState,
expressError = expressError,
onRetry = {},
)
assertThat(result.fee).isInstanceOf(FeeItemState.Empty::class.java)
}
@Test
fun `WHEN called THEN changeCardsButtonState is ENABLED`() {
val baseState = buildBaseStateWithSwapCardData(coldWallet)
val result = sut.createInitialErrorState(
fromSwapCurrencyStatus = null,
uiStateHolder = baseState,
expressError = expressError,
onRetry = {},
)
assertThat(result.changeCardsButtonState).isEqualTo(ChangeCardsButtonState.ENABLED)
}
@Test
fun `WHEN called THEN tosState is null`() {
val baseState = buildBaseStateWithSwapCardData(coldWallet)
val result = sut.createInitialErrorState(
fromSwapCurrencyStatus = null,
uiStateHolder = baseState,
expressError = expressError,
onRetry = {},
)
assertThat(result.tosState).isNull()
}
@Test
fun `GIVEN sendCardData is SwapCardData with Inputtable type WHEN called THEN sendCard type becomes disabled`() {
val baseState = buildBaseStateWithSwapCardData(coldWallet)
val result = sut.createInitialErrorState(
fromSwapCurrencyStatus = null,
uiStateHolder = baseState,
expressError = expressError,
onRetry = {},
)
val sendCard = result.sendCardData as? SwapCardState.SwapCardData
val inputtable = sendCard?.type as? TransactionCardType.Inputtable
assertThat(inputtable?.isEnabled).isFalse()
}
}
// endregion
// region createInitialLoadingState (two-arg overload)
@Nested
inner class `createInitialLoadingState two-arg overload` {
private val userWalletId = UserWalletId("aabbccdd")
private val coldWallet: UserWallet.Cold = mockk(relaxed = true) {
every { walletId } returns userWalletId
}
@Test
fun `GIVEN uiState has non-SwapCardData send card WHEN called THEN returns uiState unchanged`() {
val loadingState = sut.createInitialLoadingState()
// loadingState has SwapCardState.Empty cards — not SwapCardData
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.createInitialLoadingState(
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
uiStateHolder = loadingState,
)
assertThat(result).isSameInstanceAs(loadingState)
}
@Test
fun `GIVEN uiState has SwapCardData cards WHEN called THEN changeCardsButtonState is UPDATE_IN_PROGRESS`() {
val emptyAmountState = SwapState.EmptyAmountState(
zeroAmountEquivalent = com.tangem.core.ui.extensions.stringReference("$0.00"),
)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val readyState = sut.createInitialReadyState(
uiStateHolder = sut.createInitialLoadingState(),
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
)
val result = sut.createInitialLoadingState(
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
uiStateHolder = readyState,
)
assertThat(result.changeCardsButtonState).isEqualTo(ChangeCardsButtonState.UPDATE_IN_PROGRESS)
}
@Test
fun `GIVEN cold wallet WHEN called THEN swapButton isHoldToConfirm is false`() {
val emptyAmountState = SwapState.EmptyAmountState(
zeroAmountEquivalent = com.tangem.core.ui.extensions.stringReference("$0.00"),
)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val readyState = sut.createInitialReadyState(
uiStateHolder = sut.createInitialLoadingState(),
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
)
val result = sut.createInitialLoadingState(
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
uiStateHolder = readyState,
)
assertThat(result.swapButton.isHoldToConfirm).isFalse()
}
@Test
fun `GIVEN hot wallet WHEN called THEN swapButton isHoldToConfirm is true`() {
val hotWallet: UserWallet.Hot = mockk(relaxed = true) {
every { walletId } returns userWalletId
}
val emptyAmountState = SwapState.EmptyAmountState(
zeroAmountEquivalent = com.tangem.core.ui.extensions.stringReference("$0.00"),
)
val fromStatus = buildSwapCurrencyStatus(hotWallet)
val toStatus = buildSwapCurrencyStatus(hotWallet)
// need a base state that has SwapCardData — use readyState built with hotWallet
val readyState = sut.createInitialReadyState(
uiStateHolder = sut.createInitialLoadingState(),
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
)
val result = sut.createInitialLoadingState(
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
uiStateHolder = readyState,
)
assertThat(result.swapButton.isHoldToConfirm).isTrue()
}
}
// endregion
}
// --- Helpers shared across StateBuilder test files ---
internal fun buildSwapCurrencyStatus(
userWallet: UserWallet,
): SwapCurrencyStatus {
val userWalletId = userWallet.walletId
val account = Account.CryptoPortfolio.createMainAccount(userWalletId)
val currency: CryptoCurrency = mockk(relaxed = true) {
every { symbol } returns "ETH"
every { decimals } returns 18
every { name } returns "Ethereum"
every { network } returns mockk(relaxed = true) {
every { id } returns mockk(relaxed = true)
every { name } returns "Ethereum"
every { currencySymbol } returns "ETH"
every { rawId } returns "ethereum"
}
}
val statusValue: CryptoCurrencyStatus.Value = mockk(relaxed = true) {
every { amount } returns java.math.BigDecimal("1.0")
every { fiatRate } returns java.math.BigDecimal("2000.00")
every { fiatAmount } returns java.math.BigDecimal("2000.00")
}
val cryptoCurrencyStatus = CryptoCurrencyStatus(currency = currency, value = statusValue)
return SwapCurrencyStatus(
userWallet = userWallet,
status = cryptoCurrencyStatus,
account = account,
)
}

View file

@ -0,0 +1,408 @@
package com.tangem.feature.swap
import com.google.common.truth.Truth.assertThat
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork
import com.tangem.feature.swap.domain.models.ui.SwapState
import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.models.states.FeeItemState
import com.tangem.feature.swap.models.states.ProviderState
import com.tangem.feature.swap.models.states.SwapNotificationUM
import com.tangem.feature.swap.ui.StateBuilder
import com.tangem.utils.Provider
import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
internal class StateBuilderPairsTest {
private val actions: UiActions = mockk(relaxed = true)
private val isBalanceHiddenProvider: Provider<Boolean> = mockk()
private val appCurrencyProvider: Provider<AppCurrency> = mockk()
private val isAccountsModeProvider: Provider<Boolean> = mockk()
private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk()
private lateinit var sut: StateBuilder
private val userWalletId = UserWalletId("aabbccdd")
private val coldWallet: UserWallet.Cold = mockk(relaxed = true) {
every { walletId } returns userWalletId
}
private val hotWallet: UserWallet.Hot = mockk(relaxed = true) {
every { walletId } returns userWalletId
}
private val emptyAmountState = SwapState.EmptyAmountState(
zeroAmountEquivalent = stringReference("$0.00"),
)
@BeforeEach
fun setup() {
every { isBalanceHiddenProvider() } returns false
every { appCurrencyProvider() } returns AppCurrency.Default
every { isAccountsModeProvider() } returns false
sut = StateBuilder(
actions = actions,
isBalanceHiddenProvider = isBalanceHiddenProvider,
appCurrencyProvider = appCurrencyProvider,
isAccountsModeProvider = isAccountsModeProvider,
iGaslessFeeSupportedForNetwork = iGaslessFeeSupportedForNetwork,
)
}
// region createSwapNotSupportedState
@Nested
inner class CreateSwapNotSupportedState {
@Test
fun `GIVEN uiState sendCardData is not SwapCardData WHEN called THEN returns uiState unchanged`() {
val loadingState = sut.createInitialLoadingState()
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.createSwapNotSupportedState(
uiStateHolder = loadingState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
)
assertThat(result).isSameInstanceAs(loadingState)
}
@Test
fun `GIVEN valid SwapCardData state WHEN called THEN swapButton is disabled`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.createSwapNotSupportedState(
uiStateHolder = baseState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
)
assertThat(result.swapButton.isEnabled).isFalse()
}
@Test
fun `GIVEN valid SwapCardData state WHEN called THEN changeCardsButtonState is DISABLED`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.createSwapNotSupportedState(
uiStateHolder = baseState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
)
assertThat(result.changeCardsButtonState).isEqualTo(ChangeCardsButtonState.DISABLED)
}
@Test
fun `GIVEN valid state WHEN called THEN notifications contain SwapNotSupported warning`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.createSwapNotSupportedState(
uiStateHolder = baseState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
)
assertThat(result.notifications).hasSize(1)
assertThat(result.notifications[0]).isInstanceOf(SwapNotificationUM.Warning.SwapNotSupported::class.java)
}
@Test
fun `GIVEN valid state WHEN called THEN fee is Empty`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.createSwapNotSupportedState(
uiStateHolder = baseState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
)
assertThat(result.fee).isInstanceOf(FeeItemState.Empty::class.java)
}
@Test
fun `GIVEN valid state WHEN called THEN providerState is Empty`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.createSwapNotSupportedState(
uiStateHolder = baseState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
)
assertThat(result.providerState).isInstanceOf(ProviderState.Empty::class.java)
}
@Test
fun `GIVEN valid state with cold wallet WHEN called THEN swapButton isHoldToConfirm is false`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.createSwapNotSupportedState(
uiStateHolder = baseState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
)
assertThat(result.swapButton.isHoldToConfirm).isFalse()
}
@Test
fun `GIVEN valid state with hot wallet WHEN called THEN swapButton isHoldToConfirm is true`() {
val baseState = buildReadyState(hotWallet)
val fromStatus = buildSwapCurrencyStatus(hotWallet)
val toStatus = buildSwapCurrencyStatus(hotWallet)
val result = sut.createSwapNotSupportedState(
uiStateHolder = baseState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
)
assertThat(result.swapButton.isHoldToConfirm).isTrue()
}
@Test
fun `GIVEN valid state WHEN called THEN sendCardData type is ReadOnly`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.createSwapNotSupportedState(
uiStateHolder = baseState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
)
val sendCard = result.sendCardData as? SwapCardState.SwapCardData
assertThat(sendCard?.type).isInstanceOf(TransactionCardType.ReadOnly::class.java)
}
}
// endregion
// region updateCurrenciesState
@Nested
inner class UpdateCurrenciesState {
@Test
fun `GIVEN both currencies null WHEN called THEN sendCard is Empty`() {
val baseState = buildReadyState(coldWallet)
val result = sut.updateCurrenciesState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = null,
toSwapCurrencyStatus = null,
shouldResetAmount = false,
)
assertThat(result.sendCardData).isInstanceOf(SwapCardState.Empty::class.java)
}
@Test
fun `GIVEN both currencies non-null WHEN called THEN sendCard is SwapCardData`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.updateCurrenciesState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
shouldResetAmount = false,
)
assertThat(result.sendCardData).isInstanceOf(SwapCardState.SwapCardData::class.java)
}
@Test
fun `WHEN called THEN notifications is cleared`() {
val baseState = buildReadyState(coldWallet)
val result = sut.updateCurrenciesState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = null,
toSwapCurrencyStatus = null,
shouldResetAmount = false,
)
assertThat(result.notifications).isEmpty()
}
@Test
fun `WHEN called THEN isInsufficientFunds is false`() {
val baseState = buildReadyState(coldWallet)
val result = sut.updateCurrenciesState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = null,
toSwapCurrencyStatus = null,
shouldResetAmount = false,
)
assertThat(result.isInsufficientFunds).isFalse()
}
@Test
fun `WHEN called THEN fee is Empty`() {
val baseState = buildReadyState(coldWallet)
val result = sut.updateCurrenciesState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = null,
toSwapCurrencyStatus = null,
shouldResetAmount = false,
)
assertThat(result.fee).isInstanceOf(FeeItemState.Empty::class.java)
}
@Test
fun `WHEN called THEN changeCardsButtonState is ENABLED`() {
val baseState = buildReadyState(coldWallet)
val result = sut.updateCurrenciesState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = null,
toSwapCurrencyStatus = null,
shouldResetAmount = false,
)
assertThat(result.changeCardsButtonState).isEqualTo(ChangeCardsButtonState.ENABLED)
}
@Test
fun `GIVEN hot wallet fromStatus WHEN called THEN swapButton isHoldToConfirm is true`() {
val baseState = buildReadyState(hotWallet)
val fromStatus = buildSwapCurrencyStatus(hotWallet)
val result = sut.updateCurrenciesState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = null,
shouldResetAmount = false,
)
assertThat(result.swapButton.isHoldToConfirm).isTrue()
}
@Test
fun `GIVEN toSwapCurrencyStatus is null WHEN called THEN sendCard isEnabled is false`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.updateCurrenciesState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = null,
shouldResetAmount = false,
)
val sendCard = result.sendCardData as? SwapCardState.SwapCardData
val inputtable = sendCard?.type as? TransactionCardType.Inputtable
assertThat(inputtable?.isEnabled).isFalse()
}
}
// endregion
// region updateCurrencyBalanceStatus
@Nested
inner class UpdateCurrencyBalanceStatus {
@Test
fun `GIVEN balance hidden flag true WHEN called THEN sendCardData isBalanceHidden is true`() {
every { isBalanceHiddenProvider() } returns true
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.updateCurrencyBalanceStatus(
uiState = baseState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
emptyAmountState = emptyAmountState,
)
val sendCard = result.sendCardData as? SwapCardState.SwapCardData
assertThat(sendCard?.isBalanceHidden).isTrue()
}
@Test
fun `GIVEN balance hidden flag false WHEN called THEN sendCardData isBalanceHidden is false`() {
every { isBalanceHiddenProvider() } returns false
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.updateCurrencyBalanceStatus(
uiState = baseState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
emptyAmountState = emptyAmountState,
)
val sendCard = result.sendCardData as? SwapCardState.SwapCardData
assertThat(sendCard?.isBalanceHidden).isFalse()
}
@Test
fun `GIVEN fromStatus null WHEN called THEN sendCard becomes Empty`() {
val baseState = buildReadyState(coldWallet)
val result = sut.updateCurrencyBalanceStatus(
uiState = baseState,
fromSwapCurrencyStatus = null,
toSwapCurrencyStatus = null,
emptyAmountState = emptyAmountState,
)
assertThat(result.sendCardData).isInstanceOf(SwapCardState.Empty::class.java)
}
}
// endregion
// --- Helpers ---
private fun buildReadyState(userWallet: UserWallet): SwapStateHolder {
val fromStatus = buildSwapCurrencyStatus(userWallet)
val toStatus = buildSwapCurrencyStatus(userWallet)
return sut.createInitialReadyState(
uiStateHolder = sut.createInitialLoadingState(),
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
)
}
}

View file

@ -0,0 +1,844 @@
package com.tangem.feature.swap
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.appcurrency.model.AppCurrency
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.swap.models.SwapCurrencyStatus
import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork
import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.swap.domain.models.ui.*
import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.models.states.FeeItemState
import com.tangem.feature.swap.models.states.ProviderState
import com.tangem.feature.swap.models.states.SwapNotificationUM
import com.tangem.feature.swap.ui.StateBuilder
import com.tangem.utils.Provider
import io.mockk.every
import io.mockk.mockk
import kotlinx.collections.immutable.persistentListOf
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import java.math.BigDecimal
internal class StateBuilderQuotesTest {
private val actions: UiActions = mockk(relaxed = true)
private val isBalanceHiddenProvider: Provider<Boolean> = mockk()
private val appCurrencyProvider: Provider<AppCurrency> = mockk()
private val isAccountsModeProvider: Provider<Boolean> = mockk()
private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk()
private lateinit var sut: StateBuilder
private val userWalletId = UserWalletId("aabbccdd")
private val coldWallet: UserWallet.Cold = mockk(relaxed = true) {
every { walletId } returns userWalletId
}
private val hotWallet: UserWallet.Hot = mockk(relaxed = true) {
every { walletId } returns userWalletId
}
private val emptyAmountState = SwapState.EmptyAmountState(
zeroAmountEquivalent = com.tangem.core.ui.extensions.stringReference("$0.00"),
)
@BeforeEach
fun setup() {
every { isBalanceHiddenProvider() } returns false
every { appCurrencyProvider() } returns AppCurrency.Default
every { isAccountsModeProvider() } returns false
every { iGaslessFeeSupportedForNetwork(any()) } returns false
sut = StateBuilder(
actions = actions,
isBalanceHiddenProvider = isBalanceHiddenProvider,
appCurrencyProvider = appCurrencyProvider,
isAccountsModeProvider = isAccountsModeProvider,
iGaslessFeeSupportedForNetwork = iGaslessFeeSupportedForNetwork,
)
}
// region createQuotesLoadingState
@Nested
inner class CreateQuotesLoadingState {
@Test
fun `GIVEN uiState has Empty sendCard WHEN called THEN returns uiState unchanged`() {
val loadingState = sut.createInitialLoadingState()
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.createQuotesLoadingState(
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
uiStateHolder = loadingState,
)
assertThat(result).isSameInstanceAs(loadingState)
}
@Test
fun `GIVEN valid SwapCardData state WHEN called THEN providerState is Loading`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.createQuotesLoadingState(
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
uiStateHolder = baseState,
)
assertThat(result.providerState).isInstanceOf(ProviderState.Loading::class.java)
}
@Test
fun `GIVEN valid state WHEN called THEN changeCardsButtonState is UPDATE_IN_PROGRESS`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.createQuotesLoadingState(
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
uiStateHolder = baseState,
)
assertThat(result.changeCardsButtonState).isEqualTo(ChangeCardsButtonState.UPDATE_IN_PROGRESS)
}
@Test
fun `GIVEN valid state WHEN called THEN swapButton is disabled`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.createQuotesLoadingState(
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
uiStateHolder = baseState,
)
assertThat(result.swapButton.isEnabled).isFalse()
}
@Test
fun `GIVEN valid state WHEN called THEN fee is Empty`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.createQuotesLoadingState(
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
uiStateHolder = baseState,
)
assertThat(result.fee).isInstanceOf(FeeItemState.Empty::class.java)
}
@Test
fun `GIVEN valid state WHEN called THEN notifications is cleared`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.createQuotesLoadingState(
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
uiStateHolder = baseState,
)
assertThat(result.notifications).isEmpty()
}
@Test
fun `GIVEN hot wallet WHEN called THEN swapButton isHoldToConfirm is true`() {
val baseState = buildReadyState(hotWallet)
val fromStatus = buildSwapCurrencyStatus(hotWallet)
val toStatus = buildSwapCurrencyStatus(hotWallet)
val result = sut.createQuotesLoadingState(
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
uiStateHolder = baseState,
)
assertThat(result.swapButton.isHoldToConfirm).isTrue()
}
@Test
fun `GIVEN valid state WHEN called THEN receiveCardData amountTextFieldValue is null`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.createQuotesLoadingState(
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
uiStateHolder = baseState,
)
val receiveCard = result.receiveCardData as? SwapCardState.SwapCardData
assertThat(receiveCard?.amountTextFieldValue).isNull()
}
}
// endregion
// region createQuotesLoadedState
@Nested
inner class CreateQuotesLoadedState {
@Test
fun `GIVEN uiState has Empty sendCard WHEN called THEN returns uiState unchanged`() {
val loadingState = sut.createInitialLoadingState()
val quoteModel = buildQuoteModel(coldWallet, isBalanceEnough = true)
val swapProvider = buildSwapProvider()
val result = sut.createQuotesLoadedState(
uiStateHolder = loadingState,
quoteModel = quoteModel,
feeCryptoCurrencyStatus = null,
swapProvider = swapProvider,
bestRatedProviderId = "provider-id",
isNeedBestRateBadge = false,
selectedFeeType = FeeType.NORMAL,
needApplyFCARestrictions = false,
hideFee = false,
)
assertThat(result).isSameInstanceAs(loadingState)
}
@Test
fun `GIVEN valid state with hideFee true WHEN called THEN fee is Empty`() {
val baseState = buildReadyState(coldWallet)
val quoteModel = buildQuoteModel(coldWallet, isBalanceEnough = true)
val swapProvider = buildSwapProvider()
val result = sut.createQuotesLoadedState(
uiStateHolder = baseState,
quoteModel = quoteModel,
feeCryptoCurrencyStatus = null,
swapProvider = swapProvider,
bestRatedProviderId = "provider-id",
isNeedBestRateBadge = false,
selectedFeeType = FeeType.NORMAL,
needApplyFCARestrictions = false,
hideFee = true,
)
assertThat(result.fee).isInstanceOf(FeeItemState.Empty::class.java)
}
@Test
fun `GIVEN valid state with hideFee false and single fee WHEN called THEN fee is Content`() {
val baseState = buildReadyState(coldWallet)
val quoteModel = buildQuoteModel(
userWallet = coldWallet,
isBalanceEnough = true,
txFeeState = TxFeeState.SingleFeeState(fee = buildTxFeeLegacy(FeeType.NORMAL)),
)
val swapProvider = buildSwapProvider()
val result = sut.createQuotesLoadedState(
uiStateHolder = baseState,
quoteModel = quoteModel,
feeCryptoCurrencyStatus = null,
swapProvider = swapProvider,
bestRatedProviderId = "provider-id",
isNeedBestRateBadge = false,
selectedFeeType = FeeType.NORMAL,
needApplyFCARestrictions = false,
hideFee = false,
)
assertThat(result.fee).isInstanceOf(FeeItemState.Content::class.java)
}
@Test
fun `GIVEN valid state with sufficient balance WHEN called THEN isInsufficientFunds is false`() {
val baseState = buildReadyState(coldWallet)
val quoteModel = buildQuoteModel(coldWallet, isBalanceEnough = true)
val swapProvider = buildSwapProvider()
val result = sut.createQuotesLoadedState(
uiStateHolder = baseState,
quoteModel = quoteModel,
feeCryptoCurrencyStatus = null,
swapProvider = swapProvider,
bestRatedProviderId = "provider-id",
isNeedBestRateBadge = false,
selectedFeeType = FeeType.NORMAL,
needApplyFCARestrictions = false,
hideFee = false,
)
assertThat(result.isInsufficientFunds).isFalse()
}
@Test
fun `GIVEN valid state with insufficient balance WHEN called THEN isInsufficientFunds is true`() {
val baseState = buildReadyState(coldWallet)
val quoteModel = buildQuoteModel(
coldWallet,
isBalanceEnough = false,
includeFeeInAmount = IncludeFeeInAmount.Excluded,
)
val swapProvider = buildSwapProvider()
val result = sut.createQuotesLoadedState(
uiStateHolder = baseState,
quoteModel = quoteModel,
feeCryptoCurrencyStatus = null,
swapProvider = swapProvider,
bestRatedProviderId = "provider-id",
isNeedBestRateBadge = false,
selectedFeeType = FeeType.NORMAL,
needApplyFCARestrictions = false,
hideFee = false,
)
assertThat(result.isInsufficientFunds).isTrue()
}
@Test
fun `GIVEN valid state WHEN called THEN changeCardsButtonState is ENABLED`() {
val baseState = buildReadyState(coldWallet)
val quoteModel = buildQuoteModel(coldWallet, isBalanceEnough = true)
val swapProvider = buildSwapProvider()
val result = sut.createQuotesLoadedState(
uiStateHolder = baseState,
quoteModel = quoteModel,
feeCryptoCurrencyStatus = null,
swapProvider = swapProvider,
bestRatedProviderId = "provider-id",
isNeedBestRateBadge = false,
selectedFeeType = FeeType.NORMAL,
needApplyFCARestrictions = false,
hideFee = false,
)
assertThat(result.changeCardsButtonState).isEqualTo(ChangeCardsButtonState.ENABLED)
}
@Test
fun `GIVEN valid state with hot wallet WHEN called THEN swapButton isHoldToConfirm is true`() {
val baseState = buildReadyState(hotWallet)
val quoteModel = buildQuoteModel(hotWallet, isBalanceEnough = true)
val swapProvider = buildSwapProvider()
val result = sut.createQuotesLoadedState(
uiStateHolder = baseState,
quoteModel = quoteModel,
feeCryptoCurrencyStatus = null,
swapProvider = swapProvider,
bestRatedProviderId = "provider-id",
isNeedBestRateBadge = false,
selectedFeeType = FeeType.NORMAL,
needApplyFCARestrictions = false,
hideFee = false,
)
assertThat(result.swapButton.isHoldToConfirm).isTrue()
}
@Test
fun `GIVEN provider with termsOfUse WHEN called THEN tosState has tosLink`() {
val baseState = buildReadyState(coldWallet)
val quoteModel = buildQuoteModel(coldWallet, isBalanceEnough = true)
val swapProvider = buildSwapProvider(termsOfUse = "https://example.com/tos")
val result = sut.createQuotesLoadedState(
uiStateHolder = baseState,
quoteModel = quoteModel,
feeCryptoCurrencyStatus = null,
swapProvider = swapProvider,
bestRatedProviderId = "provider-id",
isNeedBestRateBadge = false,
selectedFeeType = FeeType.NORMAL,
needApplyFCARestrictions = false,
hideFee = false,
)
assertThat(result.tosState?.tosLink).isNotNull()
}
@Test
fun `GIVEN provider without termsOfUse WHEN called THEN tosState has null tosLink`() {
val baseState = buildReadyState(coldWallet)
val quoteModel = buildQuoteModel(coldWallet, isBalanceEnough = true)
val swapProvider = buildSwapProvider(termsOfUse = null)
val result = sut.createQuotesLoadedState(
uiStateHolder = baseState,
quoteModel = quoteModel,
feeCryptoCurrencyStatus = null,
swapProvider = swapProvider,
bestRatedProviderId = "provider-id",
isNeedBestRateBadge = false,
selectedFeeType = FeeType.NORMAL,
needApplyFCARestrictions = false,
hideFee = false,
)
assertThat(result.tosState?.tosLink).isNull()
}
@Test
fun `GIVEN no blocking notifications WHEN called THEN swapButton is enabled`() {
val baseState = buildReadyState(coldWallet)
val quoteModel = buildQuoteModel(coldWallet, isBalanceEnough = true)
val swapProvider = buildSwapProvider()
val result = sut.createQuotesLoadedState(
uiStateHolder = baseState,
quoteModel = quoteModel,
feeCryptoCurrencyStatus = null,
swapProvider = swapProvider,
bestRatedProviderId = "provider-id",
isNeedBestRateBadge = false,
selectedFeeType = FeeType.NORMAL,
needApplyFCARestrictions = false,
hideFee = false,
)
assertThat(result.swapButton.isEnabled).isTrue()
}
@Test
fun `GIVEN multiple fee state WHEN called THEN fee is Content with isClickable true`() {
val baseState = buildReadyState(coldWallet)
val quoteModel = buildQuoteModel(
userWallet = coldWallet,
isBalanceEnough = true,
txFeeState = TxFeeState.MultipleFeeState(
normalFee = buildTxFeeLegacy(FeeType.NORMAL),
priorityFee = buildTxFeeLegacy(FeeType.PRIORITY),
),
)
val swapProvider = buildSwapProvider()
val result = sut.createQuotesLoadedState(
uiStateHolder = baseState,
quoteModel = quoteModel,
feeCryptoCurrencyStatus = null,
swapProvider = swapProvider,
bestRatedProviderId = "provider-id",
isNeedBestRateBadge = false,
selectedFeeType = FeeType.NORMAL,
needApplyFCARestrictions = false,
hideFee = false,
)
val feeContent = result.fee as? FeeItemState.Content
assertThat(feeContent?.isClickable).isTrue()
}
}
// endregion
// region createQuotesErrorState
@Nested
inner class CreateQuotesErrorState {
@Test
fun `GIVEN uiState has Empty sendCard WHEN called THEN returns uiState unchanged`() {
val loadingState = sut.createInitialLoadingState()
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val fromTokenInfo = TokenSwapInfo(
tokenAmount = buildSwapAmount(),
amountFiat = BigDecimal.ZERO,
swapCurrencyStatus = fromStatus,
)
val swapProvider = buildSwapProvider()
val result = sut.createQuotesErrorState(
uiStateHolder = loadingState,
swapProvider = swapProvider,
fromToken = fromTokenInfo,
toSwapCurrencyStatus = null,
includeFeeInAmount = IncludeFeeInAmount.Excluded,
expressDataError = ExpressDataError.UnknownError,
needApplyFCARestrictions = false,
)
assertThat(result).isSameInstanceAs(loadingState)
}
@Test
fun `GIVEN valid state WHEN called THEN swapButton is disabled`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val fromTokenInfo = buildTokenSwapInfo(fromStatus)
val swapProvider = buildSwapProvider()
val result = sut.createQuotesErrorState(
uiStateHolder = baseState,
swapProvider = swapProvider,
fromToken = fromTokenInfo,
toSwapCurrencyStatus = null,
includeFeeInAmount = IncludeFeeInAmount.Excluded,
expressDataError = ExpressDataError.UnknownError,
needApplyFCARestrictions = false,
)
assertThat(result.swapButton.isEnabled).isFalse()
}
@Test
fun `GIVEN valid state WHEN called THEN fee is Empty`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val fromTokenInfo = buildTokenSwapInfo(fromStatus)
val swapProvider = buildSwapProvider()
val result = sut.createQuotesErrorState(
uiStateHolder = baseState,
swapProvider = swapProvider,
fromToken = fromTokenInfo,
toSwapCurrencyStatus = null,
includeFeeInAmount = IncludeFeeInAmount.Excluded,
expressDataError = ExpressDataError.UnknownError,
needApplyFCARestrictions = false,
)
assertThat(result.fee).isInstanceOf(FeeItemState.Empty::class.java)
}
@Test
fun `GIVEN valid state WHEN called THEN permissionUM is Empty`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val fromTokenInfo = buildTokenSwapInfo(fromStatus)
val swapProvider = buildSwapProvider()
val result = sut.createQuotesErrorState(
uiStateHolder = baseState,
swapProvider = swapProvider,
fromToken = fromTokenInfo,
toSwapCurrencyStatus = null,
includeFeeInAmount = IncludeFeeInAmount.Excluded,
expressDataError = ExpressDataError.UnknownError,
needApplyFCARestrictions = false,
)
assertThat(result.permissionUM).isEqualTo(SwapPermissionUM.Empty)
}
@Test
fun `GIVEN toSwapCurrencyStatus null WHEN called THEN receiveCardData is Empty`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val fromTokenInfo = buildTokenSwapInfo(fromStatus)
val swapProvider = buildSwapProvider()
val result = sut.createQuotesErrorState(
uiStateHolder = baseState,
swapProvider = swapProvider,
fromToken = fromTokenInfo,
toSwapCurrencyStatus = null,
includeFeeInAmount = IncludeFeeInAmount.Excluded,
expressDataError = ExpressDataError.UnknownError,
needApplyFCARestrictions = false,
)
assertThat(result.receiveCardData).isInstanceOf(SwapCardState.Empty::class.java)
}
@Test
fun `GIVEN toSwapCurrencyStatus non-null WHEN called THEN receiveCardData is SwapCardData`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val fromTokenInfo = buildTokenSwapInfo(fromStatus)
val swapProvider = buildSwapProvider()
val result = sut.createQuotesErrorState(
uiStateHolder = baseState,
swapProvider = swapProvider,
fromToken = fromTokenInfo,
toSwapCurrencyStatus = toStatus,
includeFeeInAmount = IncludeFeeInAmount.Excluded,
expressDataError = ExpressDataError.UnknownError,
needApplyFCARestrictions = false,
)
assertThat(result.receiveCardData).isInstanceOf(SwapCardState.SwapCardData::class.java)
}
@Test
fun `GIVEN ExchangeTooSmallAmountError WHEN called THEN providerState is Content`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val fromTokenInfo = buildTokenSwapInfo(fromStatus)
val swapProvider = buildSwapProvider()
val result = sut.createQuotesErrorState(
uiStateHolder = baseState,
swapProvider = swapProvider,
fromToken = fromTokenInfo,
toSwapCurrencyStatus = null,
includeFeeInAmount = IncludeFeeInAmount.Excluded,
expressDataError = ExpressDataError.ExchangeTooSmallAmountError(
amount = buildSwapAmount(),
code = 100,
),
needApplyFCARestrictions = false,
)
assertThat(result.providerState).isInstanceOf(ProviderState.Content::class.java)
}
@Test
fun `GIVEN UnknownError WHEN called THEN providerState is Empty`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val fromTokenInfo = buildTokenSwapInfo(fromStatus)
val swapProvider = buildSwapProvider()
val result = sut.createQuotesErrorState(
uiStateHolder = baseState,
swapProvider = swapProvider,
fromToken = fromTokenInfo,
toSwapCurrencyStatus = null,
includeFeeInAmount = IncludeFeeInAmount.Excluded,
expressDataError = ExpressDataError.UnknownError,
needApplyFCARestrictions = false,
)
assertThat(result.providerState).isInstanceOf(ProviderState.Empty::class.java)
}
}
// endregion
// region createQuotesEmptyAmountState
@Nested
inner class CreateQuotesEmptyAmountState {
@Test
fun `GIVEN uiState has Empty sendCard WHEN called THEN returns uiState unchanged`() {
val loadingState = sut.createInitialLoadingState()
val result = sut.createQuotesEmptyAmountState(
uiStateHolder = loadingState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = null,
)
assertThat(result).isSameInstanceAs(loadingState)
}
@Test
fun `GIVEN valid SwapCardData state WHEN called THEN swapButton is disabled`() {
val baseState = buildReadyState(coldWallet)
val result = sut.createQuotesEmptyAmountState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = null,
)
assertThat(result.swapButton.isEnabled).isFalse()
}
@Test
fun `GIVEN valid state WHEN called THEN notifications is empty`() {
val baseState = buildReadyState(coldWallet)
val result = sut.createQuotesEmptyAmountState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = null,
)
assertThat(result.notifications).isEmpty()
}
@Test
fun `GIVEN valid state WHEN called THEN isInsufficientFunds is false`() {
val baseState = buildReadyState(coldWallet)
val result = sut.createQuotesEmptyAmountState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = null,
)
assertThat(result.isInsufficientFunds).isFalse()
}
@Test
fun `GIVEN valid state WHEN called THEN fee is Empty`() {
val baseState = buildReadyState(coldWallet)
val result = sut.createQuotesEmptyAmountState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = null,
)
assertThat(result.fee).isInstanceOf(FeeItemState.Empty::class.java)
}
@Test
fun `GIVEN valid state WHEN called THEN changeCardsButtonState is ENABLED`() {
val baseState = buildReadyState(coldWallet)
val result = sut.createQuotesEmptyAmountState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = null,
)
assertThat(result.changeCardsButtonState).isEqualTo(ChangeCardsButtonState.ENABLED)
}
@Test
fun `GIVEN valid state WHEN called THEN providerState is Empty`() {
val baseState = buildReadyState(coldWallet)
val result = sut.createQuotesEmptyAmountState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = null,
)
assertThat(result.providerState).isInstanceOf(ProviderState.Empty::class.java)
}
@Test
fun `GIVEN valid state WHEN called THEN receiveCard amountTextFieldValue is 0`() {
val baseState = buildReadyState(coldWallet)
val result = sut.createQuotesEmptyAmountState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = null,
)
val receiveCard = result.receiveCardData as? SwapCardState.SwapCardData
assertThat(receiveCard?.amountTextFieldValue?.text).isEqualTo("0")
}
@Test
fun `GIVEN fromSwapCurrencyStatus with hot wallet WHEN called THEN swapButton isHoldToConfirm is true`() {
val baseState = buildReadyState(hotWallet)
val fromStatus = buildSwapCurrencyStatus(hotWallet)
val result = sut.createQuotesEmptyAmountState(
uiStateHolder = baseState,
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = fromStatus,
)
assertThat(result.swapButton.isHoldToConfirm).isTrue()
}
}
// endregion
// --- Helpers ---
private fun buildReadyState(userWallet: UserWallet): SwapStateHolder {
val fromStatus = buildSwapCurrencyStatus(userWallet)
val toStatus = buildSwapCurrencyStatus(userWallet)
return sut.createInitialReadyState(
uiStateHolder = sut.createInitialLoadingState(),
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
)
}
private fun buildQuoteModel(
userWallet: UserWallet,
isBalanceEnough: Boolean,
includeFeeInAmount: IncludeFeeInAmount = IncludeFeeInAmount.Excluded,
txFeeState: TxFeeState = TxFeeState.Empty,
): SwapState.QuotesLoadedState {
val fromStatus = buildSwapCurrencyStatus(userWallet)
val toStatus = buildSwapCurrencyStatus(userWallet)
val fromTokenInfo = TokenSwapInfo(
tokenAmount = buildSwapAmount(),
amountFiat = BigDecimal("100.00"),
swapCurrencyStatus = fromStatus,
)
val toTokenInfo = TokenSwapInfo(
tokenAmount = buildSwapAmount(value = BigDecimal("0.05")),
amountFiat = BigDecimal("100.00"),
swapCurrencyStatus = toStatus,
)
return SwapState.QuotesLoadedState(
fromTokenInfo = fromTokenInfo,
toTokenInfo = toTokenInfo,
priceImpact = PriceImpact.Empty,
preparedSwapConfigState = PreparedSwapConfigState(
isBalanceEnough = isBalanceEnough,
feeState = SwapFeeState.Enough,
hasOutgoingTransaction = false,
includeFeeInAmount = includeFeeInAmount,
),
permissionState = PermissionDataState.Empty,
txFee = txFeeState,
currencyCheck = null,
validationResult = null,
minAdaValue = null,
swapProvider = buildSwapProvider(),
)
}
private fun buildSwapProvider(
termsOfUse: String? = null,
privacyPolicy: String? = null,
) = SwapProvider(
providerId = "provider-id",
name = "TestProvider",
type = ExchangeProviderType.DEX,
imageLarge = "https://example.com/icon.png",
termsOfUse = termsOfUse,
privacyPolicy = privacyPolicy,
isRecommended = false,
slippage = null,
)
private fun buildSwapAmount(value: BigDecimal = BigDecimal("1.0")) = SwapAmount(
value = value,
decimals = 18,
)
private fun buildTokenSwapInfo(swapCurrencyStatus: SwapCurrencyStatus) = TokenSwapInfo(
tokenAmount = buildSwapAmount(),
amountFiat = BigDecimal.ZERO,
swapCurrencyStatus = swapCurrencyStatus,
)
private fun buildTxFeeLegacy(feeType: FeeType): TxFee.Legacy {
val fee: com.tangem.blockchain.common.transaction.Fee = mockk(relaxed = true)
return TxFee.Legacy(
feeValue = BigDecimal("0.001"),
feeFiatFormatted = "$2.00",
feeCryptoFormatted = "0.001 ETH",
feeIncludeOtherNativeFee = BigDecimal.ZERO,
feeFiatFormattedWithNative = "$2.00",
feeCryptoFormattedWithNative = "0.001 ETH",
cryptoSymbol = "ETH",
feeType = feeType,
fee = fee,
)
}
}

View file

@ -0,0 +1,603 @@
package com.tangem.feature.swap
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.swap.models.SwapCurrencyStatus
import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.swap.domain.models.ui.*
import com.tangem.feature.swap.model.SwapProcessDataState
import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.models.states.FeeItemState
import com.tangem.feature.swap.models.states.ProviderState
import com.tangem.feature.swap.models.states.SwapNotificationUM
import com.tangem.feature.swap.ui.StateBuilder
import com.tangem.utils.Provider
import io.mockk.every
import io.mockk.mockk
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import java.math.BigDecimal
internal class StateBuilderSwapDataTest {
private val actions: UiActions = mockk(relaxed = true)
private val isBalanceHiddenProvider: Provider<Boolean> = mockk()
private val appCurrencyProvider: Provider<AppCurrency> = mockk()
private val isAccountsModeProvider: Provider<Boolean> = mockk()
private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk()
private lateinit var sut: StateBuilder
private val userWalletId = UserWalletId("aabbccdd")
private val coldWallet: UserWallet.Cold = mockk(relaxed = true) {
every { walletId } returns userWalletId
}
private val hotWallet: UserWallet.Hot = mockk(relaxed = true) {
every { walletId } returns userWalletId
}
private val emptyAmountState = SwapState.EmptyAmountState(
zeroAmountEquivalent = com.tangem.core.ui.extensions.stringReference("$0.00"),
)
@BeforeEach
fun setup() {
every { isBalanceHiddenProvider() } returns false
every { appCurrencyProvider() } returns AppCurrency.Default
every { isAccountsModeProvider() } returns false
every { iGaslessFeeSupportedForNetwork(any()) } returns false
sut = StateBuilder(
actions = actions,
isBalanceHiddenProvider = isBalanceHiddenProvider,
appCurrencyProvider = appCurrencyProvider,
isAccountsModeProvider = isAccountsModeProvider,
iGaslessFeeSupportedForNetwork = iGaslessFeeSupportedForNetwork,
)
}
// region createSwapInProgressState
@Nested
inner class CreateSwapInProgressState {
@Test
fun `WHEN called THEN swapButton isInProgress becomes true`() {
val baseState = buildReadyState(coldWallet)
val result = sut.createSwapInProgressState(baseState)
assertThat(result.swapButton.isInProgress).isTrue()
}
@Test
fun `WHEN called THEN swapButton isEnabled becomes false`() {
val baseState = buildReadyState(coldWallet)
// force enable the button by overriding manually
val stateWithEnabled = baseState.copy(
swapButton = baseState.swapButton.copy(isEnabled = true),
)
val result = sut.createSwapInProgressState(stateWithEnabled)
assertThat(result.swapButton.isEnabled).isFalse()
}
@Test
fun `WHEN called THEN all other fields remain unchanged`() {
val baseState = buildReadyState(coldWallet)
val result = sut.createSwapInProgressState(baseState)
assertThat(result.sendCardData).isEqualTo(baseState.sendCardData)
assertThat(result.receiveCardData).isEqualTo(baseState.receiveCardData)
assertThat(result.fee).isEqualTo(baseState.fee)
assertThat(result.changeCardsButtonState).isEqualTo(baseState.changeCardsButtonState)
}
}
// endregion
// region createSilentLoadState
@Nested
inner class CreateSilentLoadState {
@Test
fun `WHEN called THEN changeCardsButtonState is UPDATE_IN_PROGRESS`() {
val baseState = buildReadyState(coldWallet)
val result = sut.createSilentLoadState(baseState)
assertThat(result.changeCardsButtonState).isEqualTo(ChangeCardsButtonState.UPDATE_IN_PROGRESS)
}
@Test
fun `GIVEN notifications without PermissionNeeded WHEN called THEN notifications remain unchanged`() {
val errorNotification = SwapNotificationUM.Warning.SwapNotSupported
val baseState = buildReadyState(coldWallet).copy(
notifications = persistentListOf(errorNotification),
)
val result = sut.createSilentLoadState(baseState)
assertThat(result.notifications).hasSize(1)
assertThat(result.notifications[0]).isEqualTo(errorNotification)
}
@Test
fun `GIVEN notifications with PermissionNeeded WHEN called THEN PermissionNeeded is removed`() {
val permissionNeeded = SwapNotificationUM.Info.PermissionNeeded(
providerName = "TestProvider",
fromTokenSymbol = "ETH",
onApproveClick = {},
)
val otherNotification = SwapNotificationUM.Warning.SwapNotSupported
val baseState = buildReadyState(coldWallet).copy(
notifications = listOf(permissionNeeded, otherNotification).toImmutableList(),
)
val result = sut.createSilentLoadState(baseState)
assertThat(result.notifications).hasSize(1)
assertThat(result.notifications[0]).isEqualTo(otherNotification)
}
}
// endregion
// region updateSwapAmount
@Nested
inner class UpdateSwapAmount {
@Test
fun `GIVEN uiState has Empty sendCard WHEN called THEN returns uiState unchanged`() {
val loadingState = sut.createInitialLoadingState()
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.updateSwapAmount(
uiState = loadingState,
amountFormatted = "1.5",
amountRaw = "1.5",
fromSwapCurrencyStatus = fromStatus,
minTxAmount = null,
)
assertThat(result).isSameInstanceAs(loadingState)
}
@Test
fun `GIVEN amount is above minTxAmount WHEN called THEN inputError is Empty`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.updateSwapAmount(
uiState = baseState,
amountFormatted = "2.0",
amountRaw = "2.0",
fromSwapCurrencyStatus = fromStatus,
minTxAmount = BigDecimal("1.0"),
)
val sendCard = result.sendCardData as? SwapCardState.SwapCardData
val inputtable = sendCard?.type as? TransactionCardType.Inputtable
assertThat(inputtable?.inputError).isEqualTo(TransactionCardType.InputError.Empty)
}
@Test
fun `GIVEN amount is below minTxAmount WHEN called THEN inputError is WrongAmount`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.updateSwapAmount(
uiState = baseState,
amountFormatted = "0.5",
amountRaw = "0.5",
fromSwapCurrencyStatus = fromStatus,
minTxAmount = BigDecimal("1.0"),
)
val sendCard = result.sendCardData as? SwapCardState.SwapCardData
val inputtable = sendCard?.type as? TransactionCardType.Inputtable
assertThat(inputtable?.inputError).isEqualTo(TransactionCardType.InputError.WrongAmount)
}
@Test
fun `GIVEN minTxAmount is null WHEN called THEN inputError is Empty`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.updateSwapAmount(
uiState = baseState,
amountFormatted = "0.001",
amountRaw = "0.001",
fromSwapCurrencyStatus = fromStatus,
minTxAmount = null,
)
val sendCard = result.sendCardData as? SwapCardState.SwapCardData
val inputtable = sendCard?.type as? TransactionCardType.Inputtable
assertThat(inputtable?.inputError).isEqualTo(TransactionCardType.InputError.Empty)
}
@Test
fun `WHEN called THEN sendCardData amountTextFieldValue text is updated`() {
val baseState = buildReadyState(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val result = sut.updateSwapAmount(
uiState = baseState,
amountFormatted = "3.14",
amountRaw = "3.14",
fromSwapCurrencyStatus = fromStatus,
minTxAmount = null,
)
val sendCard = result.sendCardData as? SwapCardState.SwapCardData
assertThat(sendCard?.amountTextFieldValue?.text).isEqualTo("3.14")
}
}
// endregion
// region updateBalanceHiddenState
@Nested
inner class UpdateBalanceHiddenState {
@Test
fun `GIVEN isBalanceHidden true WHEN called THEN sendCardData isBalanceHidden is true`() {
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val baseState = sut.createInitialReadyState(
uiStateHolder = sut.createInitialLoadingState(),
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
)
val result = sut.updateBalanceHiddenState(baseState, isBalanceHidden = true)
val sendCard = result.sendCardData as? SwapCardState.SwapCardData
assertThat(sendCard?.isBalanceHidden).isTrue()
}
@Test
fun `GIVEN isBalanceHidden true WHEN called THEN receiveCardData isBalanceHidden is true`() {
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val baseState = sut.createInitialReadyState(
uiStateHolder = sut.createInitialLoadingState(),
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
)
val result = sut.updateBalanceHiddenState(baseState, isBalanceHidden = true)
val receiveCard = result.receiveCardData as? SwapCardState.SwapCardData
assertThat(receiveCard?.isBalanceHidden).isTrue()
}
@Test
fun `GIVEN isBalanceHidden false WHEN called THEN both cards isBalanceHidden is false`() {
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val baseState = sut.createInitialReadyState(
uiStateHolder = sut.createInitialLoadingState(),
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
)
val result = sut.updateBalanceHiddenState(baseState, isBalanceHidden = false)
val sendCard = result.sendCardData as? SwapCardState.SwapCardData
val receiveCard = result.receiveCardData as? SwapCardState.SwapCardData
assertThat(sendCard?.isBalanceHidden).isFalse()
assertThat(receiveCard?.isBalanceHidden).isFalse()
}
@Test
fun `GIVEN sendCard is Empty type WHEN called THEN sendCard remains Empty type`() {
val loadingState = sut.createInitialLoadingState()
val result = sut.updateBalanceHiddenState(loadingState, isBalanceHidden = true)
assertThat(result.sendCardData).isInstanceOf(SwapCardState.Empty::class.java)
}
}
// endregion
// region loadingPermissionState
@Nested
inner class LoadingPermissionState {
@Test
fun `WHEN called THEN swapButton isEnabled is false`() {
val baseState = buildReadyState(coldWallet).copy(
swapButton = buildReadyState(coldWallet).swapButton.copy(isEnabled = true),
)
val result = sut.loadingPermissionState(baseState)
assertThat(result.swapButton.isEnabled).isFalse()
}
@Test
fun `WHEN called THEN swapButton isInProgress is false`() {
val baseState = buildReadyState(coldWallet).copy(
swapButton = buildReadyState(coldWallet).swapButton.copy(isInProgress = true),
)
val result = sut.loadingPermissionState(baseState)
assertThat(result.swapButton.isInProgress).isFalse()
}
@Test
fun `GIVEN notifications without PermissionNeeded WHEN called THEN ApprovalInProgressWarning is prepended`() {
val existingNotification = SwapNotificationUM.Warning.SwapNotSupported
val baseState = buildReadyState(coldWallet).copy(
notifications = persistentListOf(existingNotification),
)
val result = sut.loadingPermissionState(baseState)
assertThat(result.notifications[0]).isInstanceOf(SwapNotificationUM.Error.ApprovalInProgressWarning::class.java)
}
@Test
fun `GIVEN notifications with PermissionNeeded WHEN called THEN PermissionNeeded is replaced by ApprovalInProgressWarning`() {
val permissionNeeded = SwapNotificationUM.Info.PermissionNeeded(
providerName = "TestProvider",
fromTokenSymbol = "ETH",
onApproveClick = {},
)
val baseState = buildReadyState(coldWallet).copy(
notifications = persistentListOf(permissionNeeded),
)
val result = sut.loadingPermissionState(baseState)
assertThat(result.notifications).doesNotContain(permissionNeeded)
assertThat(result.notifications[0]).isInstanceOf(SwapNotificationUM.Error.ApprovalInProgressWarning::class.java)
}
}
// endregion
// region dismissBottomSheet
@Nested
inner class DismissBottomSheet {
@Test
fun `GIVEN bottomSheetConfig is null WHEN called THEN bottomSheetConfig remains null`() {
val baseState = buildReadyState(coldWallet)
assertThat(baseState.bottomSheetConfig).isNull()
val result = sut.dismissBottomSheet(baseState)
assertThat(result.bottomSheetConfig).isNull()
}
@Test
fun `GIVEN bottomSheetConfig is shown WHEN called THEN bottomSheetConfig isShown becomes false`() {
val baseState = buildReadyState(coldWallet).copy(
bottomSheetConfig = com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = mockk(relaxed = true),
),
)
val result = sut.dismissBottomSheet(baseState)
assertThat(result.bottomSheetConfig?.isShown).isFalse()
}
}
// endregion
// region addNotification
@Nested
inner class AddNotification {
@Test
fun `GIVEN a message WHEN called THEN notifications contains GenericError`() {
val baseState = buildReadyState(coldWallet)
val message = com.tangem.core.ui.extensions.stringReference("Something went wrong")
val result = sut.addNotification(
uiState = baseState,
message = message,
onClick = {},
)
assertThat(result.notifications).hasSize(1)
assertThat(result.notifications[0]).isInstanceOf(SwapNotificationUM.Error.GenericError::class.java)
}
@Test
fun `GIVEN null message WHEN called THEN notifications contains GenericError`() {
val baseState = buildReadyState(coldWallet)
val result = sut.addNotification(
uiState = baseState,
message = null,
onClick = {},
)
assertThat(result.notifications).hasSize(1)
assertThat(result.notifications[0]).isInstanceOf(SwapNotificationUM.Error.GenericError::class.java)
}
}
// endregion
// region createSuccessState
@Nested
inner class CreateSuccessState {
@Test
fun `GIVEN valid state WHEN called THEN successState is not null`() {
val baseState = buildReadyStateWithContentProvider(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val dataState = SwapProcessDataState(
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
selectedFee = null,
)
val swapTransactionState = buildSwapTransactionState()
val result = sut.createSuccessState(
uiState = baseState,
swapTransactionState = swapTransactionState,
dataState = dataState,
onExploreClick = {},
onStatusClick = {},
txUrl = "https://example.com/tx/abc",
)
assertThat(result.successState).isNotNull()
}
@Test
fun `GIVEN CEX provider WHEN called THEN shouldShowStatusButton is true`() {
val baseState = buildReadyStateWithContentProvider(
coldWallet,
providerType = ExchangeProviderType.CEX,
)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val dataState = SwapProcessDataState(
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
selectedFee = null,
)
val swapTransactionState = buildSwapTransactionState()
val result = sut.createSuccessState(
uiState = baseState,
swapTransactionState = swapTransactionState,
dataState = dataState,
onExploreClick = {},
onStatusClick = {},
txUrl = "https://example.com/tx/abc",
)
assertThat(result.successState?.shouldShowStatusButton).isTrue()
}
@Test
fun `GIVEN DEX provider WHEN called THEN shouldShowStatusButton is false`() {
val baseState = buildReadyStateWithContentProvider(
coldWallet,
providerType = ExchangeProviderType.DEX,
)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val dataState = SwapProcessDataState(
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
selectedFee = null,
)
val swapTransactionState = buildSwapTransactionState()
val result = sut.createSuccessState(
uiState = baseState,
swapTransactionState = swapTransactionState,
dataState = dataState,
onExploreClick = {},
onStatusClick = {},
txUrl = "https://example.com/tx/abc",
)
assertThat(result.successState?.shouldShowStatusButton).isFalse()
}
@Test
fun `GIVEN txUrl WHEN called THEN successState txUrl matches`() {
val baseState = buildReadyStateWithContentProvider(coldWallet)
val fromStatus = buildSwapCurrencyStatus(coldWallet)
val toStatus = buildSwapCurrencyStatus(coldWallet)
val dataState = SwapProcessDataState(
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
selectedFee = null,
)
val swapTransactionState = buildSwapTransactionState()
val expectedUrl = "https://etherscan.io/tx/0xabc"
val result = sut.createSuccessState(
uiState = baseState,
swapTransactionState = swapTransactionState,
dataState = dataState,
onExploreClick = {},
onStatusClick = {},
txUrl = expectedUrl,
)
assertThat(result.successState?.txUrl).isEqualTo(expectedUrl)
}
}
// endregion
// --- Helpers ---
private fun buildReadyState(userWallet: UserWallet): SwapStateHolder {
val fromStatus = buildSwapCurrencyStatus(userWallet)
val toStatus = buildSwapCurrencyStatus(userWallet)
return sut.createInitialReadyState(
uiStateHolder = sut.createInitialLoadingState(),
emptyAmountState = emptyAmountState,
fromSwapCurrencyStatus = fromStatus,
toSwapCurrencyStatus = toStatus,
)
}
private fun buildReadyStateWithContentProvider(
userWallet: UserWallet,
providerType: ExchangeProviderType = ExchangeProviderType.DEX,
): SwapStateHolder {
val baseState = buildReadyState(userWallet)
return baseState.copy(
providerState = ProviderState.Content(
id = "provider-id",
name = "TestProvider",
type = providerType.providerName,
iconUrl = "https://example.com/icon.png",
subtitle = com.tangem.core.ui.extensions.stringReference("1 ETH ≈ 2000 USDT"),
additionalBadge = ProviderState.AdditionalBadge.Empty,
selectionType = ProviderState.SelectionType.CLICK,
namePrefix = ProviderState.PrefixType.NONE,
onProviderClick = {},
),
)
}
private fun buildSwapTransactionState(): SwapTransactionState.TxSent {
return SwapTransactionState.TxSent(
fromAmount = "1.0 ETH",
toAmount = "2000 USDT",
fromAmountValue = BigDecimal("1.0"),
toAmountValue = BigDecimal("2000"),
txHash = "0xabc",
timestamp = System.currentTimeMillis(),
)
}
}