Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-07 22:34:21 +04:00
parent aaacc3d736
commit 54739fff06
8 changed files with 1091 additions and 163 deletions

View file

@ -0,0 +1,172 @@
package com.tangem.feature.swap.converters
import com.tangem.common.ui.swap.SwapRateFormatter
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.feature.swap.domain.models.domain.SwapProvider
import com.tangem.feature.swap.domain.models.ui.PermissionDataState
import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo
import com.tangem.feature.swap.models.states.PercentDifference
import com.tangem.feature.swap.models.states.ProviderState
/**
* Builds [ProviderState.Content] for the swap provider list / row.
*
* Pure: takes everything it needs as parameters. Designed to be unit-tested in isolation.
*/
internal object SwapProviderStateBuilder {
private val FCA_RESTRICTED_PROVIDER_IDS = setOf(
"changelly",
"changenow",
"okx-cross-chain",
"okx-on-chain",
"simpleswap",
)
/**
* Provider row on the main swap screen shows the exchange rate `1 base rate quote`
* (see [SwapRateFormatter]) and allows the user to open the provider picker.
*/
@Suppress("LongParameterList")
fun buildContentClickable(
provider: SwapProvider,
fromTokenInfo: TokenSwapInfo,
toTokenInfo: TokenSwapInfo,
permissionState: PermissionDataState,
selectionType: ProviderState.SelectionType,
isBestRate: Boolean,
isNeedBestRateBadge: Boolean,
needApplyFCARestrictions: Boolean,
onProviderClick: (String) -> Unit,
): ProviderState.Content {
val rateString = SwapRateFormatter.formatRate(
from = fromTokenInfo.swapCurrencyStatus.currency,
to = toTokenInfo.swapCurrencyStatus.currency,
fromAmount = fromTokenInfo.tokenAmount.value,
toAmount = toTokenInfo.tokenAmount.value,
)
return provider.toContent(
subtitle = stringReference(rateString),
additionalBadge = resolveBadge(
provider = provider,
needApplyFCARestrictions = needApplyFCARestrictions,
permissionState = permissionState,
isBestRate = isBestRate,
isNeedBestRateBadge = isNeedBestRateBadge,
),
selectionType = selectionType,
percentLowerThenBest = PercentDifference.Empty,
onProviderClick = onProviderClick,
)
}
/**
* Provider row in the provider-picker bottom sheet. Subtitle shows the formatted *to* amount
* (not a rate) and the row carries a percentage delta vs. the best rate.
*/
@Suppress("LongParameterList")
fun buildContentSelectable(
provider: SwapProvider,
toTokenInfo: TokenSwapInfo,
permissionState: PermissionDataState,
pricesLowerBest: Map<String, Float>,
selectionType: ProviderState.SelectionType,
needApplyFCARestrictions: Boolean,
onProviderClick: (String) -> Unit,
): ProviderState.Content {
return provider.toContent(
subtitle = buildSelectableSubtitle(toTokenInfo),
additionalBadge = resolveBadge(
provider = provider,
needApplyFCARestrictions = needApplyFCARestrictions,
permissionState = permissionState,
),
selectionType = selectionType,
percentLowerThenBest = pricesLowerBest[provider.providerId]
?.let(PercentDifference::Value)
?: PercentDifference.Value(0f),
onProviderClick = onProviderClick,
)
}
/**
* Provider row for an unavailable / errored provider subtitle is the error/alert text
* resolved by the caller.
*/
fun buildAvailableFrom(
provider: SwapProvider,
alertText: TextReference,
selectionType: ProviderState.SelectionType,
needApplyFCARestrictions: Boolean,
onProviderClick: (String) -> Unit,
): ProviderState.Content {
return provider.toContent(
subtitle = alertText,
additionalBadge = resolveBadge(
provider = provider,
needApplyFCARestrictions = needApplyFCARestrictions,
),
selectionType = selectionType,
percentLowerThenBest = PercentDifference.Empty,
onProviderClick = onProviderClick,
)
}
/**
* Subtitle (formatted *to* amount) used both for picker rows and when refreshing
* the provider-picker bottom sheet. Single source of truth so both paths stay in sync.
*/
fun buildSelectableSubtitle(toTokenInfo: TokenSwapInfo): TextReference {
val toAmount = toTokenInfo.tokenAmount.value.format {
crypto(toTokenInfo.swapCurrencyStatus.currency)
}
return stringReference(toAmount)
}
private fun resolveBadge(
provider: SwapProvider,
needApplyFCARestrictions: Boolean,
permissionState: PermissionDataState? = null,
isBestRate: Boolean = false,
isNeedBestRateBadge: Boolean = false,
): ProviderState.AdditionalBadge {
return when {
needApplyFCARestrictions && provider.isFCARestricted() ->
ProviderState.AdditionalBadge.FCAWarningList
permissionState is PermissionDataState.PermissionRequired ->
ProviderState.AdditionalBadge.PermissionRequired
provider.isRecommended ->
ProviderState.AdditionalBadge.Recommended
isNeedBestRateBadge && isBestRate && !needApplyFCARestrictions ->
ProviderState.AdditionalBadge.BestTrade
else ->
ProviderState.AdditionalBadge.Empty
}
}
private fun SwapProvider.toContent(
subtitle: TextReference,
additionalBadge: ProviderState.AdditionalBadge,
selectionType: ProviderState.SelectionType,
percentLowerThenBest: PercentDifference,
onProviderClick: (String) -> Unit,
): ProviderState.Content {
return ProviderState.Content(
id = providerId,
name = name,
iconUrl = imageLarge,
type = type.providerName,
subtitle = subtitle,
additionalBadge = additionalBadge,
selectionType = selectionType,
percentLowerThenBest = percentLowerThenBest,
namePrefix = ProviderState.PrefixType.NONE,
onProviderClick = onProviderClick,
)
}
private fun SwapProvider.isFCARestricted(): Boolean = providerId in FCA_RESTRICTED_PROVIDER_IDS
}

View file

@ -28,6 +28,7 @@ import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType
import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount
import com.tangem.feature.swap.domain.models.domain.RateType
import com.tangem.feature.swap.converters.SwapProviderStateBuilder
import com.tangem.feature.swap.domain.models.domain.SwapProvider
import com.tangem.feature.swap.domain.models.domain.SwapUIMode
import com.tangem.feature.swap.domain.models.ui.*
@ -46,8 +47,6 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal
import java.math.RoundingMode
import kotlin.math.min
/**
* State builder creates a specific states for SwapScreen
@ -549,15 +548,16 @@ internal class StateBuilder(
onClick = actions.onSwapClick,
),
changeCardsButtonState = ChangeCardsButtonState.ENABLED,
providerState = swapProvider.convertToContentClickableProviderState(
isBestRate = bestRatedProviderId == swapProvider.providerId && !priceImpact.shouldShowWarning(),
providerState = SwapProviderStateBuilder.buildContentClickable(
provider = swapProvider,
fromTokenInfo = quoteModel.fromTokenInfo,
toTokenInfo = quoteModel.toTokenInfo,
isNeedBestRateBadge = isNeedBestRateBadge,
selectionType = ProviderState.SelectionType.CLICK,
onProviderClick = actions.onProviderClick,
needApplyFCARestrictions = needApplyFCARestrictions,
permissionState = quoteModel.permissionState,
selectionType = ProviderState.SelectionType.CLICK,
isBestRate = bestRatedProviderId == swapProvider.providerId && !priceImpact.shouldShowWarning(),
isNeedBestRateBadge = isNeedBestRateBadge,
needApplyFCARestrictions = needApplyFCARestrictions,
onProviderClick = actions.onProviderClick,
),
priceImpact = priceImpact,
tosState = createTosState(swapProvider),
@ -686,27 +686,27 @@ internal class StateBuilder(
): ProviderState {
return when (expressDataError) {
is ExpressDataError.ExchangeTooSmallAmountError -> {
swapProvider.convertToAvailableFromProviderState(
swapProvider = swapProvider,
SwapProviderStateBuilder.buildAvailableFrom(
provider = swapProvider,
alertText = resourceReference(
R.string.express_provider_min_amount,
wrappedList(expressDataError.amount.getFormattedCryptoAmount(fromToken)),
),
selectionType = selectionType,
onProviderClick = onProviderClick,
needApplyFCARestrictions = needApplyFCARestrictions,
onProviderClick = onProviderClick,
)
}
is ExpressDataError.ExchangeTooBigAmountError -> {
swapProvider.convertToAvailableFromProviderState(
swapProvider = swapProvider,
SwapProviderStateBuilder.buildAvailableFrom(
provider = swapProvider,
alertText = resourceReference(
R.string.express_provider_max_amount,
wrappedList(expressDataError.amount.getFormattedCryptoAmount(fromToken)),
),
selectionType = selectionType,
onProviderClick = onProviderClick,
needApplyFCARestrictions = needApplyFCARestrictions,
onProviderClick = onProviderClick,
)
}
else -> {
@ -1053,10 +1053,8 @@ internal class StateBuilder(
providers = providers.map { providerState ->
val tokenInfo = tokenSwapInfoForProviders[providerState.id]
if (providerState is ProviderState.Content && tokenInfo != null) {
val rateString = tokenInfo.tokenAmount
.getFormattedCryptoAmount(tokenInfo.swapCurrencyStatus.currency)
providerState.copy(
subtitle = stringReference(rateString),
subtitle = SwapProviderStateBuilder.buildSelectableSubtitle(tokenInfo),
percentLowerThenBest = pricesLowerBest[providerState.id]?.let { percent ->
PercentDifference.Value(percent)
} ?: PercentDifference.Value(0f),
@ -1135,12 +1133,14 @@ internal class StateBuilder(
return when (val state = this.value) {
is SwapState.EmptyAmountState -> null
is SwapState.QuotesLoadedState -> {
provider.convertToContentSelectableProviderState(
state = state,
onProviderClick = onProviderSelect,
SwapProviderStateBuilder.buildContentSelectable(
provider = provider,
toTokenInfo = state.toTokenInfo,
permissionState = state.permissionState,
pricesLowerBest = pricesLowerBest,
selectionType = ProviderState.SelectionType.SELECT,
needApplyFCARestrictions = needApplyFCARestrictions,
onProviderClick = onProviderSelect,
)
}
is SwapState.SwapError -> getProviderStateForError(
@ -1154,113 +1154,6 @@ internal class StateBuilder(
}
}
@Suppress("LongParameterList")
private fun SwapProvider.convertToContentClickableProviderState(
isBestRate: Boolean,
fromTokenInfo: TokenSwapInfo,
toTokenInfo: TokenSwapInfo,
selectionType: ProviderState.SelectionType,
isNeedBestRateBadge: Boolean,
onProviderClick: (String) -> Unit,
needApplyFCARestrictions: Boolean,
permissionState: PermissionDataState,
): ProviderState {
val rate = toTokenInfo.tokenAmount.value.calculateRate(
fromTokenInfo.tokenAmount.value,
toTokenInfo.swapCurrencyStatus.currency.decimals,
)
val fromCurrencySymbol = fromTokenInfo.swapCurrencyStatus.currency.symbol
val rateString = buildString {
append(BigDecimal.ONE.format { crypto(symbol = fromCurrencySymbol, decimals = 0).anyDecimals() })
append("")
append(rate.format { crypto(toTokenInfo.swapCurrencyStatus.currency) })
}
val additionalBadge = when {
needApplyFCARestrictions && isFCARestrictedProvider() -> ProviderState.AdditionalBadge.FCAWarningList
permissionState is PermissionDataState.PermissionRequired ->
ProviderState.AdditionalBadge.PermissionRequired
isRecommended -> ProviderState.AdditionalBadge.Recommended
isNeedBestRateBadge && isBestRate && !needApplyFCARestrictions -> ProviderState.AdditionalBadge.BestTrade
else -> ProviderState.AdditionalBadge.Empty
}
return ProviderState.Content(
id = this.providerId,
name = this.name,
iconUrl = this.imageLarge,
type = this.type.providerName,
subtitle = stringReference(rateString),
additionalBadge = additionalBadge,
selectionType = selectionType,
percentLowerThenBest = PercentDifference.Empty,
namePrefix = ProviderState.PrefixType.NONE,
onProviderClick = onProviderClick,
)
}
private fun SwapProvider.convertToContentSelectableProviderState(
state: SwapState.QuotesLoadedState,
selectionType: ProviderState.SelectionType,
pricesLowerBest: Map<String, Float>,
onProviderClick: (String) -> Unit,
needApplyFCARestrictions: Boolean,
): ProviderState {
val toTokenInfo = state.toTokenInfo
val rateString = toTokenInfo.tokenAmount.getFormattedCryptoAmount(toTokenInfo.swapCurrencyStatus.currency)
val additionalBadge = when {
needApplyFCARestrictions && isFCARestrictedProvider() -> ProviderState.AdditionalBadge.FCAWarningList
state.permissionState is PermissionDataState.PermissionRequired -> {
ProviderState.AdditionalBadge.PermissionRequired
}
isRecommended -> ProviderState.AdditionalBadge.Recommended
else -> ProviderState.AdditionalBadge.Empty
}
return ProviderState.Content(
id = this.providerId,
name = this.name,
iconUrl = this.imageLarge,
type = this.type.providerName,
subtitle = stringReference(rateString),
additionalBadge = additionalBadge,
selectionType = selectionType,
percentLowerThenBest = pricesLowerBest[this.providerId]?.let { percent ->
PercentDifference.Value(percent)
} ?: PercentDifference.Value(0f),
namePrefix = ProviderState.PrefixType.NONE,
onProviderClick = onProviderClick,
)
}
private fun SwapProvider.convertToAvailableFromProviderState(
swapProvider: SwapProvider,
alertText: TextReference,
selectionType: ProviderState.SelectionType,
onProviderClick: (String) -> Unit,
needApplyFCARestrictions: Boolean,
): ProviderState {
val additionalBadge = when {
needApplyFCARestrictions && isFCARestrictedProvider() -> ProviderState.AdditionalBadge.FCAWarningList
swapProvider.isRecommended -> ProviderState.AdditionalBadge.Recommended
else -> ProviderState.AdditionalBadge.Empty
}
return ProviderState.Content(
id = this.providerId,
name = this.name,
iconUrl = this.imageLarge,
type = this.type.providerName,
selectionType = selectionType,
subtitle = alertText,
additionalBadge = additionalBadge,
percentLowerThenBest = PercentDifference.Empty,
namePrefix = ProviderState.PrefixType.NONE,
onProviderClick = onProviderClick,
)
}
private fun CryptoCurrencyStatus?.getFormattedAmount(isNeedSymbol: Boolean): String {
val amount = this?.value?.amount ?: return DASH_SIGN
val symbol = if (isNeedSymbol) currency.symbol else ""
@ -1284,19 +1177,10 @@ internal class StateBuilder(
return value.format { crypto(token) }
}
private fun BigDecimal.calculateRate(to: BigDecimal, decimals: Int): BigDecimal {
val rateDecimals = if (decimals == 0) IF_ZERO_DECIMALS_TO_SHOW else decimals
return this.divide(to, min(rateDecimals, MAX_DECIMALS_TO_SHOW), RoundingMode.HALF_UP)
}
private fun String.appendApproximateSign(): String {
return "$TILDE_SIGN $this"
}
private fun SwapProvider.isFCARestrictedProvider(): Boolean {
return FCA_RESTRICTED_PROVIDER_IDS.contains(providerId)
}
private fun getCardAccountTitle(account: Account?, isFromCard: Boolean): AccountTitleUM {
val (prefix, placeholder) = if (isFromCard) {
R.string.swapping_from_account_title to R.string.swapping_from_title_v2
@ -1320,17 +1204,4 @@ internal class StateBuilder(
is Account.Payment -> AccountIconUM.Payment
}
}
private companion object {
private const val MAX_DECIMALS_TO_SHOW = 8
private const val IF_ZERO_DECIMALS_TO_SHOW = 2
private val FCA_RESTRICTED_PROVIDER_IDS = setOf(
"changelly",
"changenow",
"okx-cross-chain",
"okx-on-chain",
"simpleswap",
)
}
}

View file

@ -0,0 +1,372 @@
package com.tangem.feature.swap.converters
import com.google.common.truth.Truth.assertThat
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType
import com.tangem.feature.swap.domain.models.domain.SwapProvider
import com.tangem.feature.swap.domain.models.ui.PermissionDataState
import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo
import com.tangem.domain.swap.models.SwapCurrencyStatus
import com.tangem.feature.swap.models.states.PercentDifference
import com.tangem.feature.swap.models.states.ProviderState
import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.math.BigDecimal
import java.util.Locale
internal class SwapProviderStateBuilderTest {
private var originalLocale: Locale = Locale.getDefault()
private val onProviderClick: (String) -> Unit = {}
@BeforeEach
fun setUp() {
originalLocale = Locale.getDefault()
Locale.setDefault(Locale.US)
}
@AfterEach
fun tearDown() {
Locale.setDefault(originalLocale)
}
// region buildContentClickable
@Test
fun `GIVEN best rate AND no FCA AND no permission WHEN buildContentClickable THEN BestTrade badge`() {
val provider = provider(id = "1inch", isRecommended = false)
val from = tokenInfo(symbol = "ETH", decimals = 18, amount = BigDecimal.ONE)
val to = tokenInfo(symbol = "USDT", decimals = 6, amount = BigDecimal("3000"))
val result = SwapProviderStateBuilder.buildContentClickable(
provider = provider,
fromTokenInfo = from,
toTokenInfo = to,
permissionState = PermissionDataState.Empty,
selectionType = ProviderState.SelectionType.CLICK,
isBestRate = true,
isNeedBestRateBadge = true,
needApplyFCARestrictions = false,
onProviderClick = onProviderClick,
)
assertThat(result.additionalBadge).isEqualTo(ProviderState.AdditionalBadge.BestTrade)
assertThat(result.percentLowerThenBest).isEqualTo(PercentDifference.Empty)
assertThat(result.subtitle).isInstanceOf(TextReference.Str::class.java)
val subtitle = result.subtitle as TextReference.Str
assertThat(subtitle.value).contains("ETH")
assertThat(subtitle.value).contains("USDT")
}
@Test
fun `GIVEN recommended provider WHEN buildContentClickable THEN Recommended badge`() {
val provider = provider(id = "any", isRecommended = true)
val info = tokenInfo(symbol = "ETH", decimals = 18, amount = BigDecimal.ONE)
val result = SwapProviderStateBuilder.buildContentClickable(
provider = provider,
fromTokenInfo = info,
toTokenInfo = info,
permissionState = PermissionDataState.Empty,
selectionType = ProviderState.SelectionType.CLICK,
isBestRate = true,
isNeedBestRateBadge = true,
needApplyFCARestrictions = false,
onProviderClick = onProviderClick,
)
assertThat(result.additionalBadge).isEqualTo(ProviderState.AdditionalBadge.Recommended)
}
@Test
fun `GIVEN permission required WHEN buildContentClickable THEN PermissionRequired badge`() {
val provider = provider(id = "any", isRecommended = false)
val info = tokenInfo(symbol = "ETH", decimals = 18, amount = BigDecimal.ONE)
val result = SwapProviderStateBuilder.buildContentClickable(
provider = provider,
fromTokenInfo = info,
toTokenInfo = info,
permissionState = PermissionDataState.PermissionRequired(
isResetApproval = false,
spenderAddress = "0xspender",
),
selectionType = ProviderState.SelectionType.CLICK,
isBestRate = true,
isNeedBestRateBadge = true,
needApplyFCARestrictions = false,
onProviderClick = onProviderClick,
)
assertThat(result.additionalBadge).isEqualTo(ProviderState.AdditionalBadge.PermissionRequired)
}
@Test
fun `GIVEN FCA restricted provider WHEN buildContentClickable THEN FCAWarningList badge`() {
val provider = provider(id = "changelly", isRecommended = true)
val info = tokenInfo(symbol = "ETH", decimals = 18, amount = BigDecimal.ONE)
val result = SwapProviderStateBuilder.buildContentClickable(
provider = provider,
fromTokenInfo = info,
toTokenInfo = info,
permissionState = PermissionDataState.PermissionRequired(
isResetApproval = false,
spenderAddress = "0xspender",
),
selectionType = ProviderState.SelectionType.CLICK,
isBestRate = true,
isNeedBestRateBadge = true,
needApplyFCARestrictions = true,
onProviderClick = onProviderClick,
)
assertThat(result.additionalBadge).isEqualTo(ProviderState.AdditionalBadge.FCAWarningList)
}
@Test
fun `GIVEN best rate badge disabled WHEN buildContentClickable THEN Empty badge`() {
val provider = provider(id = "any", isRecommended = false)
val info = tokenInfo(symbol = "ETH", decimals = 18, amount = BigDecimal.ONE)
val result = SwapProviderStateBuilder.buildContentClickable(
provider = provider,
fromTokenInfo = info,
toTokenInfo = info,
permissionState = PermissionDataState.Empty,
selectionType = ProviderState.SelectionType.CLICK,
isBestRate = true,
isNeedBestRateBadge = false,
needApplyFCARestrictions = false,
onProviderClick = onProviderClick,
)
assertThat(result.additionalBadge).isEqualTo(ProviderState.AdditionalBadge.Empty)
}
@Test
fun `GIVEN provider WHEN buildContentClickable THEN content carries provider identity`() {
val provider = provider(id = "1inch", isRecommended = false, name = "1inch", iconUrl = "https://x")
val info = tokenInfo(symbol = "ETH", decimals = 18, amount = BigDecimal.ONE)
val result = SwapProviderStateBuilder.buildContentClickable(
provider = provider,
fromTokenInfo = info,
toTokenInfo = info,
permissionState = PermissionDataState.Empty,
selectionType = ProviderState.SelectionType.CLICK,
isBestRate = false,
isNeedBestRateBadge = false,
needApplyFCARestrictions = false,
onProviderClick = onProviderClick,
)
assertThat(result.id).isEqualTo("1inch")
assertThat(result.name).isEqualTo("1inch")
assertThat(result.iconUrl).isEqualTo("https://x")
assertThat(result.type).isEqualTo("DEX")
assertThat(result.selectionType).isEqualTo(ProviderState.SelectionType.CLICK)
assertThat(result.namePrefix).isEqualTo(ProviderState.PrefixType.NONE)
}
// endregion
// region buildContentSelectable
@Test
fun `GIVEN provider in pricesLowerBest WHEN buildContentSelectable THEN percentLowerThenBest is mapped`() {
val provider = provider(id = "1inch", isRecommended = false)
val info = tokenInfo(symbol = "USDT", decimals = 6, amount = BigDecimal("100"))
val result = SwapProviderStateBuilder.buildContentSelectable(
provider = provider,
toTokenInfo = info,
permissionState = PermissionDataState.Empty,
pricesLowerBest = mapOf("1inch" to 0.5f),
selectionType = ProviderState.SelectionType.SELECT,
needApplyFCARestrictions = false,
onProviderClick = onProviderClick,
)
assertThat(result.percentLowerThenBest).isEqualTo(PercentDifference.Value(0.5f))
assertThat(result.subtitle).isInstanceOf(TextReference.Str::class.java)
val subtitle = result.subtitle as TextReference.Str
assertThat(subtitle.value).contains("USDT")
}
@Test
fun `GIVEN provider not in pricesLowerBest WHEN buildContentSelectable THEN percentLowerThenBest is zero`() {
val provider = provider(id = "any", isRecommended = false)
val info = tokenInfo(symbol = "USDT", decimals = 6, amount = BigDecimal("100"))
val result = SwapProviderStateBuilder.buildContentSelectable(
provider = provider,
toTokenInfo = info,
permissionState = PermissionDataState.Empty,
pricesLowerBest = emptyMap(),
selectionType = ProviderState.SelectionType.SELECT,
needApplyFCARestrictions = false,
onProviderClick = onProviderClick,
)
assertThat(result.percentLowerThenBest).isEqualTo(PercentDifference.Value(0f))
}
@Test
fun `GIVEN best rate badge inputs WHEN buildContentSelectable THEN BestTrade badge is never set`() {
val provider = provider(id = "any", isRecommended = false)
val info = tokenInfo(symbol = "USDT", decimals = 6, amount = BigDecimal("100"))
val result = SwapProviderStateBuilder.buildContentSelectable(
provider = provider,
toTokenInfo = info,
permissionState = PermissionDataState.Empty,
pricesLowerBest = emptyMap(),
selectionType = ProviderState.SelectionType.SELECT,
needApplyFCARestrictions = false,
onProviderClick = onProviderClick,
)
assertThat(result.additionalBadge).isEqualTo(ProviderState.AdditionalBadge.Empty)
}
@Test
fun `GIVEN permission required WHEN buildContentSelectable THEN PermissionRequired badge`() {
val provider = provider(id = "any", isRecommended = false)
val info = tokenInfo(symbol = "USDT", decimals = 6, amount = BigDecimal("100"))
val result = SwapProviderStateBuilder.buildContentSelectable(
provider = provider,
toTokenInfo = info,
permissionState = PermissionDataState.PermissionRequired(
isResetApproval = false,
spenderAddress = "0xspender",
),
pricesLowerBest = emptyMap(),
selectionType = ProviderState.SelectionType.SELECT,
needApplyFCARestrictions = false,
onProviderClick = onProviderClick,
)
assertThat(result.additionalBadge).isEqualTo(ProviderState.AdditionalBadge.PermissionRequired)
}
// endregion
// region buildAvailableFrom
@Test
fun `GIVEN alert text WHEN buildAvailableFrom THEN subtitle is the alert text`() {
val provider = provider(id = "any", isRecommended = false)
val alert: TextReference = stringReference("min amount 0.01 ETH")
val result = SwapProviderStateBuilder.buildAvailableFrom(
provider = provider,
alertText = alert,
selectionType = ProviderState.SelectionType.SELECT,
needApplyFCARestrictions = false,
onProviderClick = onProviderClick,
)
assertThat(result.subtitle).isEqualTo(alert)
assertThat(result.percentLowerThenBest).isEqualTo(PercentDifference.Empty)
}
@Test
fun `GIVEN FCA restricted WHEN buildAvailableFrom THEN FCAWarningList badge`() {
val provider = provider(id = "okx-on-chain", isRecommended = true)
val result = SwapProviderStateBuilder.buildAvailableFrom(
provider = provider,
alertText = TextReference.EMPTY,
selectionType = ProviderState.SelectionType.SELECT,
needApplyFCARestrictions = true,
onProviderClick = onProviderClick,
)
assertThat(result.additionalBadge).isEqualTo(ProviderState.AdditionalBadge.FCAWarningList)
}
@Test
fun `GIVEN recommended WHEN buildAvailableFrom THEN Recommended badge`() {
val provider = provider(id = "any", isRecommended = true)
val result = SwapProviderStateBuilder.buildAvailableFrom(
provider = provider,
alertText = TextReference.EMPTY,
selectionType = ProviderState.SelectionType.SELECT,
needApplyFCARestrictions = false,
onProviderClick = onProviderClick,
)
assertThat(result.additionalBadge).isEqualTo(ProviderState.AdditionalBadge.Recommended)
}
@Test
fun `GIVEN no flags WHEN buildAvailableFrom THEN Empty badge`() {
val provider = provider(id = "any", isRecommended = false)
val result = SwapProviderStateBuilder.buildAvailableFrom(
provider = provider,
alertText = TextReference.EMPTY,
selectionType = ProviderState.SelectionType.SELECT,
needApplyFCARestrictions = false,
onProviderClick = onProviderClick,
)
assertThat(result.additionalBadge).isEqualTo(ProviderState.AdditionalBadge.Empty)
}
// endregion
// region buildSelectableSubtitle
@Test
fun `GIVEN to token info WHEN buildSelectableSubtitle THEN string contains symbol`() {
val info = tokenInfo(symbol = "USDT", decimals = 6, amount = BigDecimal("100"))
val result = SwapProviderStateBuilder.buildSelectableSubtitle(info)
assertThat(result).isInstanceOf(TextReference.Str::class.java)
val subtitle = result as TextReference.Str
assertThat(subtitle.value).contains("USDT")
assertThat(subtitle.value).contains("100")
}
// endregion
private fun provider(
id: String,
isRecommended: Boolean,
name: String = "Provider",
iconUrl: String = "https://icon",
): SwapProvider = mockk {
every { providerId } returns id
every { this@mockk.name } returns name
every { imageLarge } returns iconUrl
every { type } returns ExchangeProviderType.DEX
every { this@mockk.isRecommended } returns isRecommended
}
private fun tokenInfo(symbol: String, decimals: Int, amount: BigDecimal): TokenSwapInfo {
val currency = mockk<CryptoCurrency.Coin> {
every { this@mockk.symbol } returns symbol
every { this@mockk.decimals } returns decimals
}
val swapStatus = mockk<SwapCurrencyStatus> {
every { this@mockk.currency } returns currency
}
return TokenSwapInfo(
tokenAmount = SwapAmount(value = amount, decimals = decimals),
amountFiat = BigDecimal.ZERO,
swapCurrencyStatus = swapStatus,
)
}
}