Updated on 2026-08-14
This commit is contained in:
commit
0b86264bbf
20 changed files with 285 additions and 131 deletions
|
|
@ -46,6 +46,7 @@ internal sealed class SwapAmountUM {
|
|||
// selected swap route
|
||||
val swapRateType: ExpressRateType,
|
||||
val swapRateMode: SwapRateMode,
|
||||
val priceImpact: PriceImpact?,
|
||||
|
||||
// swap models
|
||||
val swapCurrencies: SwapCurrencies,
|
||||
|
|
@ -79,7 +80,6 @@ sealed class SwapAmountFieldUM {
|
|||
data class Content(
|
||||
override val amountType: SwapAmountType,
|
||||
override val amountField: AmountState,
|
||||
val priceImpact: TextReference?,
|
||||
val title: TextReference,
|
||||
val subtitleLeft: TextReference,
|
||||
val subtitleRight: TextReference,
|
||||
|
|
@ -90,9 +90,33 @@ sealed class SwapAmountFieldUM {
|
|||
}
|
||||
|
||||
@Immutable
|
||||
sealed class PriceImpactUM {
|
||||
data class PriceImpact(
|
||||
val value: TextReference,
|
||||
val amountSignificance: AmountSignificance,
|
||||
val type: Type,
|
||||
) {
|
||||
|
||||
data object Empty : PriceImpactUM()
|
||||
enum class Type {
|
||||
NONE, LOW, MEDIUM, HIGH
|
||||
}
|
||||
|
||||
data class Value(val value: Float) : PriceImpactUM()
|
||||
enum class AmountSignificance {
|
||||
LOW, MEDIUM, HIGH
|
||||
}
|
||||
|
||||
fun shouldDisableButton(): Boolean {
|
||||
return type == Type.HIGH && amountSignificance == AmountSignificance.HIGH
|
||||
}
|
||||
|
||||
fun shouldShowWarning(): Boolean {
|
||||
return type.ordinal > Type.LOW.ordinal || amountSignificance.ordinal > AmountSignificance.LOW.ordinal
|
||||
}
|
||||
|
||||
companion object {
|
||||
val Empty = PriceImpact(
|
||||
value = TextReference.EMPTY,
|
||||
amountSignificance = AmountSignificance.LOW,
|
||||
type = Type.NONE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -21,13 +21,14 @@ import com.tangem.datasource.local.swap.SwapBestRateAnimationStore
|
|||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.express.models.ExpressError
|
||||
import com.tangem.domain.express.models.ExpressRateType
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.notifications.ShouldShowNotificationUseCase
|
||||
import com.tangem.domain.quotes.GetCurrencyUSDQuoteUseCase
|
||||
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
|
||||
import com.tangem.domain.settings.usercountry.models.UserCountry
|
||||
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
|
||||
import com.tangem.domain.express.models.ExpressRateType
|
||||
import com.tangem.domain.swap.models.*
|
||||
import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection
|
||||
import com.tangem.domain.swap.usecase.GetSwapQuoteUseCase
|
||||
|
|
@ -58,13 +59,13 @@ import com.tangem.utils.coroutines.PeriodicTask
|
|||
import com.tangem.utils.coroutines.SingleTaskScheduler
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import com.tangem.utils.transformer.update
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import java.math.BigDecimal
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
|
@ -93,6 +94,7 @@ internal class SwapAmountModel @Inject constructor(
|
|||
private val shouldShowNotificationUseCase: ShouldShowNotificationUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val getWalletsUseCase: GetWalletsUseCase,
|
||||
private val getCurrencyUSDQuoteUseCase: GetCurrencyUSDQuoteUseCase,
|
||||
) : Model(), SwapAmountClickIntents, SwapChooseProviderComponent.ModelCallback {
|
||||
|
||||
private val params: SwapAmountComponentParams = paramsContainer.require()
|
||||
|
|
@ -107,6 +109,9 @@ internal class SwapAmountModel @Inject constructor(
|
|||
private var secondaryMaximumAmountBoundary: EnterAmountBoundary? = null
|
||||
private var secondaryMinimumAmountBoundary: EnterAmountBoundary? = null
|
||||
|
||||
private var primaryFiatRateUSD: BigDecimal? = null
|
||||
private var secondaryFiatRateUSD: BigDecimal? = null
|
||||
|
||||
private var userCountry: UserCountry = UserCountry.Other(Locale.getDefault().country)
|
||||
val bottomSheetNavigation: SlotNavigation<SwapChooseProviderConfig> = SlotNavigation()
|
||||
val rateInfoNavigation: SlotNavigation<ExpressRateType> = SlotNavigation()
|
||||
|
|
@ -177,6 +182,8 @@ internal class SwapAmountModel @Inject constructor(
|
|||
isBalanceHidden = params.isBalanceHidingFlow.value,
|
||||
primaryMaximumAmountBoundary = primaryMaximumAmountBoundary,
|
||||
primaryMinimumAmountBoundary = primaryMinimumAmountBoundary,
|
||||
primaryFiatRateUSD = primaryFiatRateUSD,
|
||||
secondaryFiatRateUSD = secondaryFiatRateUSD,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -218,7 +225,7 @@ internal class SwapAmountModel @Inject constructor(
|
|||
val selectedProvider = amountUM.selectedQuote.provider ?: return
|
||||
|
||||
swapAmountAlertFactory.priceImpactAlert(
|
||||
hasPriceImpact = (amountUM.secondaryAmount as? SwapAmountFieldUM.Content)?.priceImpact != null,
|
||||
hasPriceImpact = amountUM.priceImpact != null,
|
||||
currencySymbol = amountUM.primaryCryptoCurrencyStatus.currency.symbol,
|
||||
provider = selectedProvider,
|
||||
)
|
||||
|
|
@ -560,6 +567,8 @@ internal class SwapAmountModel @Inject constructor(
|
|||
val secondaryStatus = secondaryCurrency?.currencyStatus
|
||||
val primaryStatus = (uiState.value as? SwapAmountUM.Content)?.primaryCryptoCurrencyStatus
|
||||
if (secondaryStatus != null && primaryStatus != null) {
|
||||
val rawId = secondaryStatus.currency.id.rawCurrencyId
|
||||
secondaryFiatRateUSD = rawId?.let { id -> getCurrencyUSDQuoteUseCase(id) }
|
||||
initCurrencies(primaryStatus, secondaryStatus)
|
||||
val isOnlyOneWallet = getWalletsUseCase.invokeSync().size == 1
|
||||
uiState.transformerUpdate(
|
||||
|
|
@ -611,7 +620,13 @@ internal class SwapAmountModel @Inject constructor(
|
|||
)
|
||||
primaryMaximumAmountBoundary = MaxEnterAmountConverter().convert(primaryStatus)
|
||||
|
||||
val primaryRawId = primaryStatus.currency.id.rawCurrencyId
|
||||
primaryFiatRateUSD = primaryRawId?.let { id -> getCurrencyUSDQuoteUseCase(id) }
|
||||
|
||||
if (secondaryStatus != null) {
|
||||
val secondaryRawId = secondaryStatus.currency.id.rawCurrencyId
|
||||
secondaryFiatRateUSD = secondaryRawId?.let { id -> getCurrencyUSDQuoteUseCase(id) }
|
||||
|
||||
secondaryMinimumAmountBoundary = EnterAmountBoundary(
|
||||
amount = getMinimumTransactionAmountSyncUseCase
|
||||
.invoke(
|
||||
|
|
@ -727,6 +742,8 @@ internal class SwapAmountModel @Inject constructor(
|
|||
isBalanceHidden = params.isBalanceHidingFlow.value,
|
||||
primaryMaximumAmountBoundary = primaryMaximumAmountBoundary,
|
||||
primaryMinimumAmountBoundary = primaryMinimumAmountBoundary,
|
||||
primaryFiatRateUSD = primaryFiatRateUSD,
|
||||
secondaryFiatRateUSD = secondaryFiatRateUSD,
|
||||
),
|
||||
)
|
||||
feeSelectorReloadTrigger.triggerUpdate()
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
package com.tangem.features.swap.v2.impl.amount.model
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.swap.models.SwapAmountType
|
||||
import com.tangem.domain.swap.models.SwapDirection
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.PriceImpact
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
|
||||
import com.tangem.utils.extensions.isZero
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import kotlin.math.min
|
||||
|
|
@ -20,31 +21,74 @@ internal object SwapAmountQuoteUtils {
|
|||
private const val MAX_DECIMALS = 8
|
||||
private const val MIN_DECIMALS = 2
|
||||
|
||||
fun calculatePriceImpact(
|
||||
fromTokenAmount: BigDecimal,
|
||||
toTokenAmount: BigDecimal,
|
||||
private val PRICE_IMPACT_AMOUNT_MIN_THRESHOLD = 25.toBigDecimal() // in USD
|
||||
private val PRICE_IMPACT_AMOUNT_MAX_THRESHOLD = 5000.toBigDecimal() // in USD
|
||||
private val PRICE_IMPACT_LOW_THRESHOLD = 0.1.toBigDecimal() // 10%
|
||||
private val PRICE_IMPACT_HIGH_THRESHOLD = 0.5.toBigDecimal() // 50%
|
||||
|
||||
@Suppress("ComplexMethod", "LongParameterList")
|
||||
internal fun calculatePriceImpact(
|
||||
quoteContent: SwapQuoteUM.Content?,
|
||||
swapDirection: SwapDirection,
|
||||
primaryFiatRateUSD: BigDecimal?,
|
||||
secondaryFiatRateUSD: BigDecimal?,
|
||||
primaryCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
secondaryCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
): TextReference? {
|
||||
secondaryCryptoCurrencyStatus: CryptoCurrencyStatus?,
|
||||
): PriceImpact? {
|
||||
if (quoteContent == null) return null
|
||||
|
||||
val fromAmount = quoteContent.fromAmount ?: return null
|
||||
val toAmount = quoteContent.toAmount
|
||||
|
||||
val (fromRate, toRate) = if (swapDirection == SwapDirection.Direct) {
|
||||
primaryCryptoCurrencyStatus.value.fiatRate to secondaryCryptoCurrencyStatus.value.fiatRate
|
||||
primaryCryptoCurrencyStatus.value.fiatRate to secondaryCryptoCurrencyStatus?.value?.fiatRate
|
||||
} else {
|
||||
secondaryCryptoCurrencyStatus.value.fiatRate to primaryCryptoCurrencyStatus.value.fiatRate
|
||||
secondaryCryptoCurrencyStatus?.value?.fiatRate to primaryCryptoCurrencyStatus.value.fiatRate
|
||||
}
|
||||
|
||||
val isRatesNull = fromRate.isNullOrZero() || toRate.isNullOrZero()
|
||||
val isAmountNull = fromTokenAmount.isZero() || toTokenAmount.isZero()
|
||||
if (isRatesNull || isAmountNull) return null
|
||||
|
||||
val fromTokenFiatValue = fromTokenAmount.multiply(fromRate)
|
||||
val toTokenFiatValue = toTokenAmount.multiply(toRate)
|
||||
|
||||
val value = BigDecimal.ONE - toTokenFiatValue.divide(fromTokenFiatValue, 2, RoundingMode.HALF_UP)
|
||||
|
||||
return stringReference("$(-${value.format { percent(withoutSign = false) }})").takeIf {
|
||||
value > 0.1.toBigDecimal()
|
||||
val fromRateUsd = if (swapDirection == SwapDirection.Direct) {
|
||||
primaryFiatRateUSD
|
||||
} else {
|
||||
secondaryFiatRateUSD
|
||||
}
|
||||
|
||||
val fromTokenFiatValue = fromRate?.let { fromAmount.multiply(fromRate).orZero() }
|
||||
val toTokenFiatValue = toRate?.let { toAmount.multiply(toRate) }
|
||||
|
||||
val isFromNotZero = fromTokenFiatValue != null && fromTokenFiatValue != BigDecimal.ZERO
|
||||
val isToNotZero = toTokenFiatValue != null && toTokenFiatValue != BigDecimal.ZERO
|
||||
|
||||
val value = if (isFromNotZero && isToNotZero) {
|
||||
BigDecimal.ONE - toTokenFiatValue.divide(fromTokenFiatValue, 2, RoundingMode.HALF_UP)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val fromAmountUSD = fromAmount.multiply(fromRateUsd.orZero())
|
||||
|
||||
val type = when {
|
||||
value == null -> PriceImpact.Type.NONE
|
||||
value < PRICE_IMPACT_LOW_THRESHOLD -> PriceImpact.Type.LOW
|
||||
value in PRICE_IMPACT_LOW_THRESHOLD..PRICE_IMPACT_HIGH_THRESHOLD -> PriceImpact.Type.MEDIUM
|
||||
else -> PriceImpact.Type.HIGH
|
||||
}
|
||||
|
||||
val amountSignificance = when {
|
||||
fromAmountUSD <= PRICE_IMPACT_AMOUNT_MIN_THRESHOLD -> PriceImpact.AmountSignificance.LOW
|
||||
fromAmountUSD > PRICE_IMPACT_AMOUNT_MAX_THRESHOLD -> PriceImpact.AmountSignificance.HIGH
|
||||
else -> PriceImpact.AmountSignificance.MEDIUM
|
||||
}
|
||||
|
||||
return PriceImpact(
|
||||
value = stringReference("(${StringsSigns.MINUS}${value.format { percent() }})"),
|
||||
amountSignificance = amountSignificance,
|
||||
type = type,
|
||||
)
|
||||
}
|
||||
|
||||
fun isHighPriceImpact(amountUM: SwapAmountUM): Boolean {
|
||||
val priceImpact = (amountUM as? SwapAmountUM.Content)?.priceImpact ?: return false
|
||||
return priceImpact.shouldDisableButton()
|
||||
}
|
||||
|
||||
fun calculateRate(fromAmount: BigDecimal, toAmount: BigDecimal, toAmountDecimals: Int): BigDecimal {
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ internal class SwapAmountFieldConverter(
|
|||
subtitleEllipsisLeft = subtitles.subtitleEllipsisLeft,
|
||||
subtitleRight = subtitles.subtitleRight,
|
||||
subtitleEllipsisRight = subtitles.subtitleEllipsisRight,
|
||||
priceImpact = null,
|
||||
isClickEnabled = true,
|
||||
amountField = AmountStateConverter(
|
||||
clickIntents = clickIntents,
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ internal class SwapAmountPrimaryReadyStateTransformer(
|
|||
appCurrency = appCurrency,
|
||||
isShowBestRateAnimation = isShowBestRateAnimation,
|
||||
isShowFCAWarning = false,
|
||||
priceImpact = null,
|
||||
swapRateMode = (prevState as? SwapAmountUM.Content)?.swapRateMode ?: SwapRateMode.FLOAT_ONLY,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ internal class SwapAmountSecondaryReadyStateTransformer(
|
|||
appCurrency = appCurrency,
|
||||
isShowBestRateAnimation = isShowBestRateAnimation,
|
||||
isShowFCAWarning = false,
|
||||
priceImpact = null,
|
||||
swapRateMode = swapRateMode,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,17 +7,18 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.domain.swap.models.SwapAmountType
|
||||
import com.tangem.features.swap.v2.impl.R
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
|
||||
import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.calculatePriceImpact
|
||||
import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountErrorConverter
|
||||
import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountUpdateSubtitleConverter
|
||||
import com.tangem.features.swap.v2.impl.R
|
||||
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
|
||||
import com.tangem.features.swap.v2.impl.common.isRestrictedByFCA
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class SwapAmountSelectQuoteTransformer(
|
||||
private val quoteUM: SwapQuoteUM,
|
||||
private val secondaryMaximumAmountBoundary: EnterAmountBoundary?,
|
||||
|
|
@ -26,35 +27,64 @@ internal class SwapAmountSelectQuoteTransformer(
|
|||
private val isBalanceHidden: Boolean,
|
||||
private val primaryMaximumAmountBoundary: EnterAmountBoundary? = null,
|
||||
private val primaryMinimumAmountBoundary: EnterAmountBoundary? = null,
|
||||
private val primaryFiatRateUSD: BigDecimal?,
|
||||
private val secondaryFiatRateUSD: BigDecimal?,
|
||||
) : Transformer<SwapAmountUM> {
|
||||
|
||||
@Suppress("CyclomaticComplexMethod", "LongMethod")
|
||||
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
|
||||
if (prevState !is SwapAmountUM.Content) return prevState
|
||||
|
||||
val isPrimarySelected = prevState.selectedAmountType == SwapAmountType.From
|
||||
val isSecondarySelected = prevState.selectedAmountType == SwapAmountType.To
|
||||
|
||||
val primaryProviderErrorConverter = SwapAmountErrorConverter(
|
||||
cryptoCurrency = prevState.primaryCryptoCurrencyStatus.currency,
|
||||
)
|
||||
val secondaryProviderErrorConverter = prevState.secondaryCryptoCurrencyStatus?.let {
|
||||
SwapAmountErrorConverter(cryptoCurrency = it.currency)
|
||||
}
|
||||
|
||||
val quoteContent = quoteUM as? SwapQuoteUM.Content
|
||||
val fromAmount = quoteContent?.fromAmount
|
||||
val toAmount = quoteContent?.toAmount
|
||||
|
||||
val primarySwapAmountField = prevState.primaryAmount as? SwapAmountFieldUM.Content
|
||||
val secondarySwapAmountField = prevState.secondaryAmount as? SwapAmountFieldUM.Content
|
||||
|
||||
val subtitleConverter = SwapAmountUpdateSubtitleConverter(
|
||||
selectedAmountType = prevState.selectedAmountType,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
|
||||
val newPrimaryAmount = when {
|
||||
val newPrimaryAmount = getPrimaryAmount(
|
||||
prevState = prevState,
|
||||
quoteContent = quoteContent,
|
||||
subtitleConverter = subtitleConverter,
|
||||
)
|
||||
|
||||
val newSecondaryAmount = getSecondaryAmount(
|
||||
prevState = prevState,
|
||||
quoteContent = quoteContent,
|
||||
subtitleConverter = subtitleConverter,
|
||||
)
|
||||
|
||||
val priceImpact = calculatePriceImpact(
|
||||
quoteContent = quoteContent,
|
||||
swapDirection = prevState.swapDirection,
|
||||
primaryFiatRateUSD = primaryFiatRateUSD,
|
||||
secondaryFiatRateUSD = secondaryFiatRateUSD,
|
||||
primaryCryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus,
|
||||
secondaryCryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus,
|
||||
)
|
||||
|
||||
return prevState.copy(
|
||||
isPrimaryButtonEnabled = quoteUM is SwapQuoteUM.Content,
|
||||
selectedQuote = quoteUM,
|
||||
isShowFCAWarning = isNeedApplyFCARestrictions && quoteUM.provider?.isRestrictedByFCA() == true,
|
||||
primaryAmount = newPrimaryAmount,
|
||||
priceImpact = priceImpact,
|
||||
secondaryAmount = newSecondaryAmount,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getPrimaryAmount(
|
||||
prevState: SwapAmountUM.Content,
|
||||
quoteContent: SwapQuoteUM.Content?,
|
||||
subtitleConverter: SwapAmountUpdateSubtitleConverter,
|
||||
): SwapAmountFieldUM {
|
||||
val isPrimarySelected = prevState.selectedAmountType == SwapAmountType.From
|
||||
val primarySwapAmountField = prevState.primaryAmount as? SwapAmountFieldUM.Content
|
||||
|
||||
val primaryProviderErrorConverter = SwapAmountErrorConverter(
|
||||
cryptoCurrency = prevState.primaryCryptoCurrencyStatus.currency,
|
||||
)
|
||||
val fromAmount = quoteContent?.fromAmount
|
||||
return when {
|
||||
fromAmount != null && primaryMaximumAmountBoundary != null -> {
|
||||
primarySwapAmountField?.let { fromField ->
|
||||
subtitleConverter.updateSubtitles(
|
||||
|
|
@ -103,22 +133,26 @@ internal class SwapAmountSelectQuoteTransformer(
|
|||
}
|
||||
else -> prevState.primaryAmount
|
||||
}
|
||||
}
|
||||
|
||||
val newSecondaryAmount = if (
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private fun getSecondaryAmount(
|
||||
prevState: SwapAmountUM.Content,
|
||||
quoteContent: SwapQuoteUM.Content?,
|
||||
subtitleConverter: SwapAmountUpdateSubtitleConverter,
|
||||
): SwapAmountFieldUM {
|
||||
val isSecondarySelected = prevState.selectedAmountType == SwapAmountType.To
|
||||
val secondaryProviderErrorConverter = prevState.secondaryCryptoCurrencyStatus?.let {
|
||||
SwapAmountErrorConverter(cryptoCurrency = it.currency)
|
||||
}
|
||||
val fromAmount = quoteContent?.fromAmount
|
||||
val toAmount = quoteContent?.toAmount
|
||||
val secondarySwapAmountField = prevState.secondaryAmount as? SwapAmountFieldUM.Content
|
||||
return if (
|
||||
prevState.secondaryCryptoCurrencyStatus != null &&
|
||||
secondaryMaximumAmountBoundary != null &&
|
||||
secondarySwapAmountField != null
|
||||
) {
|
||||
val fromAmountForPriceImpact = fromAmount
|
||||
?: (prevState.primaryAmount.amountField as? AmountState.Data)
|
||||
?.amountTextField?.cryptoAmount?.value.orZero()
|
||||
val priceImpact = calculatePriceImpact(
|
||||
swapDirection = prevState.swapDirection,
|
||||
fromTokenAmount = fromAmountForPriceImpact,
|
||||
toTokenAmount = toAmount.orZero(),
|
||||
primaryCryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus,
|
||||
secondaryCryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus,
|
||||
)
|
||||
val isAmountEmpty = toAmount == null
|
||||
val secondaryAmountError = if (isSecondarySelected) {
|
||||
(quoteUM as? SwapQuoteUM.Error)?.expressError
|
||||
|
|
@ -166,19 +200,10 @@ internal class SwapAmountSelectQuoteTransformer(
|
|||
isAmountEmpty = isAmountEmpty,
|
||||
displayAmount = toAmount,
|
||||
).copy(
|
||||
priceImpact = priceImpact,
|
||||
amountField = secondaryAmountFieldWithError,
|
||||
)
|
||||
} else {
|
||||
prevState.secondaryAmount
|
||||
}
|
||||
|
||||
return prevState.copy(
|
||||
isPrimaryButtonEnabled = quoteUM is SwapQuoteUM.Content,
|
||||
selectedQuote = quoteUM,
|
||||
isShowFCAWarning = isNeedApplyFCARestrictions && quoteUM.provider?.isRestrictedByFCA() == true,
|
||||
primaryAmount = newPrimaryAmount,
|
||||
secondaryAmount = newSecondaryAmount,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
import kotlinx.collections.immutable.toPersistentList
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class SwapAmountSetQuotesTransformer(
|
||||
private val quotes: List<SwapQuoteUM>,
|
||||
private val secondaryMaximumAmountBoundary: EnterAmountBoundary?,
|
||||
|
|
@ -27,6 +28,8 @@ internal class SwapAmountSetQuotesTransformer(
|
|||
private val isBalanceHidden: Boolean,
|
||||
private val primaryMaximumAmountBoundary: EnterAmountBoundary? = null,
|
||||
private val primaryMinimumAmountBoundary: EnterAmountBoundary? = null,
|
||||
private val primaryFiatRateUSD: BigDecimal?,
|
||||
private val secondaryFiatRateUSD: BigDecimal?,
|
||||
) : Transformer<SwapAmountUM> {
|
||||
|
||||
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
|
||||
|
|
@ -53,6 +56,8 @@ internal class SwapAmountSetQuotesTransformer(
|
|||
|
||||
val updatedState = SwapAmountSelectQuoteTransformer(
|
||||
quoteUM = selectedQuote,
|
||||
primaryFiatRateUSD = primaryFiatRateUSD,
|
||||
secondaryFiatRateUSD = secondaryFiatRateUSD,
|
||||
secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary,
|
||||
secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary,
|
||||
isNeedApplyFCARestrictions = isNeedApplyFcaRestrictions &&
|
||||
|
|
|
|||
|
|
@ -37,9 +37,9 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.swap.v2.impl.R
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM
|
||||
import com.tangem.domain.swap.models.SwapAmountType
|
||||
import com.tangem.features.swap.v2.impl.R
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.PriceImpact
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
|
||||
import com.tangem.features.swap.v2.impl.amount.ui.preview.SwapAmountContentPreview
|
||||
import com.tangem.features.swap.v2.impl.chooseprovider.ui.SwapChooseProviderContent
|
||||
|
|
@ -86,7 +86,7 @@ internal fun SwapAmountBlockContent(
|
|||
val quoteContent = amountUM.selectedQuote as? SwapQuoteUM.Content
|
||||
val isBestRate = quoteContent?.diffPercent is SwapQuoteUM.Content.DifferencePercent.Best
|
||||
SwapChooseProviderContent(
|
||||
isBestRate = isBestRate,
|
||||
isBestRate = isBestRate && amountUM.priceImpact?.shouldShowWarning() != true,
|
||||
isSingleProvider = quoteContent?.isSingleProvider == true,
|
||||
showBestRateAnimation = amountUM.isShowBestRateAnimation,
|
||||
expressProvider = amountUM.selectedQuote.provider,
|
||||
|
|
@ -119,13 +119,6 @@ private fun ConstraintLayoutScope.SwapAmountBlock(
|
|||
start.linkTo(parent.start)
|
||||
end.linkTo(parent.end)
|
||||
},
|
||||
extraContent = {
|
||||
SwapPriceImpact(
|
||||
amountFieldUM = amountUM.primaryAmount,
|
||||
selectedAmountType = amountUM.selectedAmountType,
|
||||
onInfoClick = onInfoClick,
|
||||
)
|
||||
},
|
||||
)
|
||||
AmountBlockV2(
|
||||
amountState = (amountUM.secondaryAmount.amountField as? AmountState.Data)?.copy(
|
||||
|
|
@ -140,35 +133,33 @@ private fun ConstraintLayoutScope.SwapAmountBlock(
|
|||
end.linkTo(parent.end)
|
||||
},
|
||||
extraContent = {
|
||||
SwapPriceImpact(
|
||||
amountFieldUM = amountUM.secondaryAmount,
|
||||
selectedAmountType = amountUM.selectedAmountType,
|
||||
onInfoClick = onInfoClick,
|
||||
)
|
||||
if (amountUM.secondaryAmount.amountType == SwapAmountType.To) {
|
||||
SwapPriceImpact(
|
||||
priceImpact = amountUM.priceImpact,
|
||||
onInfoClick = onInfoClick,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SwapPriceImpact(
|
||||
amountFieldUM: SwapAmountFieldUM,
|
||||
selectedAmountType: SwapAmountType,
|
||||
onInfoClick: () -> Unit,
|
||||
) {
|
||||
if (amountFieldUM.amountType == selectedAmountType) return
|
||||
|
||||
val priceImpact = (amountFieldUM as? SwapAmountFieldUM.Content)?.priceImpact
|
||||
val iconColor = if (priceImpact != null) {
|
||||
TangemTheme.colors.icon.attention
|
||||
} else {
|
||||
TangemTheme.colors.icon.informative
|
||||
private fun SwapPriceImpact(priceImpact: PriceImpact?, onInfoClick: () -> Unit) {
|
||||
val iconColor = when (priceImpact?.type) {
|
||||
PriceImpact.Type.MEDIUM -> TangemTheme.colors.icon.attention
|
||||
PriceImpact.Type.HIGH -> TangemTheme.colors.text.warning
|
||||
else -> TangemTheme.colors.icon.informative
|
||||
}
|
||||
|
||||
if (priceImpact != null) {
|
||||
if (priceImpact != null && priceImpact.type.ordinal > PriceImpact.Type.LOW.ordinal) {
|
||||
Text(
|
||||
text = priceImpact.resolveReference(),
|
||||
text = priceImpact.value.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.attention,
|
||||
color = when (priceImpact.type) {
|
||||
PriceImpact.Type.HIGH -> TangemTheme.colors.text.warning
|
||||
PriceImpact.Type.MEDIUM -> TangemTheme.colors.text.attention
|
||||
else -> TangemTheme.colors.text.tertiary
|
||||
},
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ import com.tangem.domain.express.models.ExpressRateType
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.swap.models.SwapAmountType
|
||||
import com.tangem.domain.swap.models.SwapCurrencies
|
||||
import com.tangem.domain.swap.models.SwapDirection
|
||||
import com.tangem.domain.swap.models.SwapRateMode
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM
|
||||
import com.tangem.domain.swap.models.SwapAmountType
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
|
||||
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
|
||||
import com.tangem.utils.StringsSigns
|
||||
|
|
@ -95,6 +95,7 @@ internal data object SwapAmountContentPreview {
|
|||
appCurrency = AppCurrency.Default,
|
||||
isShowBestRateAnimation = false,
|
||||
isShowFCAWarning = false,
|
||||
priceImpact = null,
|
||||
swapRateMode = SwapRateMode.FLOAT_ONLY,
|
||||
)
|
||||
|
||||
|
|
@ -105,7 +106,6 @@ internal data object SwapAmountContentPreview {
|
|||
title = stringReference("Tether"),
|
||||
subtitleLeft = stringReference("11 101,123123456 BTC"),
|
||||
subtitleRight = stringReference(" ${StringsSigns.DOT} 1 212,12 $"),
|
||||
priceImpact = null,
|
||||
isClickEnabled = false,
|
||||
subtitleEllipsisLeft = TextEllipsis.OffsetEnd(3),
|
||||
subtitleEllipsisRight = TextEllipsis.OffsetEnd(1),
|
||||
|
|
@ -116,7 +116,6 @@ internal data object SwapAmountContentPreview {
|
|||
accountTitleUM = AccountTitleUM.Text(stringReference("Amount to receive")),
|
||||
),
|
||||
title = stringReference("Shiba Inu"),
|
||||
priceImpact = stringReference("(-10%)"),
|
||||
subtitleLeft = TextReference.EMPTY,
|
||||
subtitleRight = TextReference.EMPTY,
|
||||
isClickEnabled = false,
|
||||
|
|
@ -135,6 +134,7 @@ internal data object SwapAmountContentPreview {
|
|||
isPrimaryButtonEnabled = true,
|
||||
isShowBestRateAnimation = false,
|
||||
isShowFCAWarning = true,
|
||||
priceImpact = null,
|
||||
swapRateMode = SwapRateMode.FLOAT_AND_FIXED,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.domain.models.account.Account
|
|||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.swap.models.SwapAmountType
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.PriceImpact
|
||||
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
|
@ -24,4 +25,5 @@ internal data class ConfirmData(
|
|||
val quote: SwapQuoteUM?,
|
||||
val rateType: ExpressRateType?,
|
||||
val amountType: SwapAmountType,
|
||||
val priceImpact: PriceImpact?,
|
||||
)
|
||||
|
|
@ -9,6 +9,7 @@ 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.models.wallet.UserWalletId
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.PriceImpact
|
||||
import java.math.BigDecimal
|
||||
import com.tangem.features.swap.v2.impl.notifications.model.SwapNotificationsModel
|
||||
import com.tangem.features.swap.v2.impl.notifications.ui.swapNotifications
|
||||
|
|
@ -49,6 +50,7 @@ internal class SwapNotificationsComponent(
|
|||
val userWalletId: UserWalletId? = null,
|
||||
val enteredFromAmount: BigDecimal? = null,
|
||||
val fromCryptoCurrencyStatus: CryptoCurrencyStatus? = null,
|
||||
val priceImpact: PriceImpact? = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -82,5 +82,16 @@ internal object SwapNotificationUM {
|
|||
onClick = onConfirmClick,
|
||||
),
|
||||
)
|
||||
|
||||
data object HighPriceImpact : Warning(
|
||||
title = resourceReference(R.string.swapping_high_price_impact_title),
|
||||
subtitle = resourceReference(R.string.swapping_high_price_impact_text),
|
||||
)
|
||||
|
||||
data object TradeTooHigh : Warning(
|
||||
title = resourceReference(R.string.swapping_trade_too_large_title),
|
||||
subtitle = resourceReference(R.string.swapping_trade_too_large_text),
|
||||
iconResId = R.drawable.ic_alert_circle_24,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,13 +11,13 @@ import com.tangem.domain.express.models.ExpressError
|
|||
import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase
|
||||
import com.tangem.domain.utils.convertToSdkAmount
|
||||
import com.tangem.features.swap.v2.api.subcomponents.SwapAmountUpdateTrigger
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.PriceImpact
|
||||
import com.tangem.features.swap.v2.impl.notifications.DefaultSwapNotificationsUpdateTrigger
|
||||
import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent
|
||||
import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent.Params.SwapNotificationData
|
||||
import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsUpdateListener
|
||||
import com.tangem.features.swap.v2.impl.notifications.entity.SwapNotificationUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import java.math.BigDecimal
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
@ -26,6 +26,7 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
|
|
@ -68,9 +69,16 @@ internal class SwapNotificationsModel @Inject constructor(
|
|||
addInsufficientFundsNotification()
|
||||
addExpressErrorNotification()
|
||||
addDestinationTagRequiredNotification()
|
||||
maybeAddPriceImpactNotification()
|
||||
}
|
||||
|
||||
swapNotificationsUpdateTrigger.callbackHasError(notifications.isNotEmpty())
|
||||
val hasErrorNotification = notifications
|
||||
.filterNot { notification ->
|
||||
notification == SwapNotificationUM.Warning.TradeTooHigh ||
|
||||
notification == SwapNotificationUM.Warning.HighPriceImpact
|
||||
}
|
||||
.isNotEmpty()
|
||||
swapNotificationsUpdateTrigger.callbackHasError(hasErrorNotification)
|
||||
uiState.value = notifications.toImmutableList()
|
||||
}
|
||||
|
||||
|
|
@ -135,4 +143,17 @@ internal class SwapNotificationsModel @Inject constructor(
|
|||
|
||||
add(errorNotification)
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.maybeAddPriceImpactNotification() {
|
||||
val priceImpact = notificationData.priceImpact ?: return
|
||||
if (!priceImpact.shouldShowWarning()) return
|
||||
|
||||
val notification = when (priceImpact.type) {
|
||||
PriceImpact.Type.HIGH -> SwapNotificationUM.Warning.TradeTooHigh
|
||||
PriceImpact.Type.MEDIUM -> SwapNotificationUM.Warning.HighPriceImpact
|
||||
else -> return
|
||||
}
|
||||
|
||||
add(notification)
|
||||
}
|
||||
}
|
||||
|
|
@ -38,6 +38,7 @@ internal fun LazyListScope.swapNotifications(
|
|||
else -> TangemTheme.colors.button.disabled
|
||||
},
|
||||
iconTint = when (item) {
|
||||
is SwapNotificationUM.Warning.TradeTooHigh -> TangemTheme.colors.icon.warning
|
||||
is SwapNotificationUM.Error,
|
||||
is NotificationUM.Error.TokenExceedsBalance,
|
||||
is NotificationUM.Warning,
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor(
|
|||
userWalletId = params.userWallet.walletId,
|
||||
enteredFromAmount = model.confirmData.enteredFromAmount,
|
||||
fromCryptoCurrencyStatus = model.confirmData.fromCryptoCurrencyStatus,
|
||||
priceImpact = model.confirmData.priceImpact,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ import com.tangem.features.swap.v2.api.subcomponents.SwapAmountUpdateTrigger
|
|||
import com.tangem.features.swap.v2.impl.R
|
||||
import com.tangem.features.swap.v2.impl.amount.SwapAmountReduceTrigger
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
|
||||
import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils
|
||||
import com.tangem.features.swap.v2.impl.common.ConfirmData
|
||||
import com.tangem.features.swap.v2.impl.common.SwapAlertFactory
|
||||
import com.tangem.features.swap.v2.impl.common.SwapUtils.INCREASE_GAS_LIMIT_FOR_CEX
|
||||
|
|
@ -126,7 +127,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
|
|||
private val feeUMV2
|
||||
get() = uiState.value.feeSelectorUM as? FeeSelectorUMRedesigned.Content
|
||||
|
||||
val secondaryCurrencyStatus: CryptoCurrencyStatus? = amountUM?.secondaryCryptoCurrencyStatus
|
||||
private val secondaryCurrencyStatus: CryptoCurrencyStatus? = amountUM?.secondaryCryptoCurrencyStatus
|
||||
val secondaryCurrency: CryptoCurrency = requireNotNull(amountUM?.secondaryCryptoCurrencyStatus?.currency) {
|
||||
"Crypto currency must not be null"
|
||||
}
|
||||
|
|
@ -166,6 +167,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
|
|||
quote = amountUM?.selectedQuote,
|
||||
rateType = amountUM?.swapRateType,
|
||||
amountType = amountUM?.selectedAmountType ?: SwapAmountType.From,
|
||||
priceImpact = amountUM?.priceImpact,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -444,6 +446,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
|
|||
userWalletId = params.userWallet.walletId,
|
||||
enteredFromAmount = confirmData.enteredFromAmount,
|
||||
fromCryptoCurrencyStatus = confirmData.fromCryptoCurrencyStatus,
|
||||
priceImpact = confirmData.priceImpact,
|
||||
),
|
||||
)
|
||||
uiState.transformerUpdate(
|
||||
|
|
@ -461,9 +464,11 @@ internal class SendWithSwapConfirmModel @Inject constructor(
|
|||
uiState.update { state ->
|
||||
val feeUM = state.feeSelectorUM as? FeeSelectorUM.Content
|
||||
val isTransactionInProcess = (state.confirmUM as? ConfirmUM.Content)?.isTransactionInProcess == true
|
||||
val isHighPriceImpact = SwapAmountQuoteUtils.isHighPriceImpact(state.amountUM)
|
||||
val isPrimaryButtonEnabled = !hasError && feeUM != null && !isTransactionInProcess && !isHighPriceImpact
|
||||
state.copy(
|
||||
confirmUM = (state.confirmUM as? ConfirmUM.Content)?.copy(
|
||||
isPrimaryButtonEnabled = !hasError && feeUM != null && !isTransactionInProcess,
|
||||
isPrimaryButtonEnabled = isPrimaryButtonEnabled,
|
||||
) ?: state.confirmUM,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@ import com.tangem.feature.swap.domain.models.toStringWithRightOffset
|
|||
import com.tangem.feature.swap.domain.models.ui.*
|
||||
import com.tangem.lib.crypto.BlockchainUtils.SOLANA_TRANSACTION_SIZE_THRESHOLD_BYTES
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
|
@ -2457,7 +2456,8 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
if (fromQuoteStatus == null) return PriceImpact.Empty
|
||||
|
||||
val fromTokenFiatValue = fromTokenAmount.multiply(fromQuoteStatus.fiatRate)
|
||||
val toTokenFiatValue = toTokenAmount.multiply(toRate.orZero())
|
||||
val toTokenFiatValue = toRate?.let { toTokenAmount.multiply(toRate) } ?: return PriceImpact.Empty
|
||||
|
||||
val value = BigDecimal.ONE - toTokenFiatValue.divide(fromTokenFiatValue, 2, RoundingMode.HALF_UP)
|
||||
|
||||
val fromAmountUSD = if (fromQuoteStatus.fiatRateUSD != BigDecimal.ZERO) {
|
||||
|
|
@ -2467,7 +2467,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
}
|
||||
|
||||
val amountSignificance = when {
|
||||
fromAmountUSD < PRICE_IMPACT_AMOUNT_MIN_THRESHOLD -> PriceImpact.AmountSignificance.LOW
|
||||
fromAmountUSD <= PRICE_IMPACT_AMOUNT_MIN_THRESHOLD -> PriceImpact.AmountSignificance.LOW
|
||||
fromAmountUSD > PRICE_IMPACT_AMOUNT_MAX_THRESHOLD -> PriceImpact.AmountSignificance.HIGH
|
||||
else -> PriceImpact.AmountSignificance.MEDIUM
|
||||
}
|
||||
|
|
@ -2552,7 +2552,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
companion object {
|
||||
private const val INCREASE_GAS_LIMIT_FOR_DEX = 112 // 12%
|
||||
private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5%
|
||||
private val PRICE_IMPACT_AMOUNT_MIN_THRESHOLD = 50.toBigDecimal() // in USD
|
||||
private val PRICE_IMPACT_AMOUNT_MIN_THRESHOLD = 25.toBigDecimal() // in USD
|
||||
private val PRICE_IMPACT_AMOUNT_MAX_THRESHOLD = 5000.toBigDecimal() // in USD
|
||||
private val PRICE_IMPACT_LOW_THRESHOLD = 0.1.toBigDecimal() // 10%
|
||||
private val PRICE_IMPACT_HIGH_THRESHOLD = 0.5.toBigDecimal() // 50%
|
||||
|
|
|
|||
|
|
@ -66,6 +66,10 @@ data class PriceImpact(
|
|||
return type == Type.HIGH && amountSignificance == AmountSignificance.HIGH
|
||||
}
|
||||
|
||||
fun shouldShowWarning(): Boolean {
|
||||
return type.ordinal > Type.LOW.ordinal || amountSignificance.ordinal > AmountSignificance.LOW.ordinal
|
||||
}
|
||||
|
||||
companion object {
|
||||
val Empty = PriceImpact(
|
||||
value = BigDecimal.ZERO,
|
||||
|
|
|
|||
|
|
@ -387,26 +387,26 @@ internal class StateBuilder(
|
|||
.formatToUIRepresentation()
|
||||
.appendApproximateSign(),
|
||||
),
|
||||
amountEquivalent = combinedReference(
|
||||
getFormattedFiatAmount(quoteModel.toTokenInfo.amountFiat),
|
||||
stringReference(StringsSigns.WHITE_SPACE),
|
||||
styledStringReference(
|
||||
priceImpact.value.format {
|
||||
percent(withoutSign = false)
|
||||
},
|
||||
spanStyleReference = {
|
||||
SpanStyle(
|
||||
color = when (priceImpact.type) {
|
||||
PriceImpact.Type.HIGH -> TangemTheme.colors.text.warning
|
||||
PriceImpact.Type.MEDIUM -> TangemTheme.colors.text.attention
|
||||
PriceImpact.Type.LOW,
|
||||
PriceImpact.Type.NONE,
|
||||
-> TangemTheme.colors.text.tertiary
|
||||
},
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
amountEquivalent = if (priceImpact.type.ordinal > PriceImpact.Type.LOW.ordinal) {
|
||||
combinedReference(
|
||||
getFormattedFiatAmount(quoteModel.toTokenInfo.amountFiat),
|
||||
stringReference(StringsSigns.WHITE_SPACE),
|
||||
styledStringReference(
|
||||
value = "(${StringsSigns.MINUS}${priceImpact.value.format { percent() }})",
|
||||
spanStyleReference = {
|
||||
SpanStyle(
|
||||
color = when (priceImpact.type) {
|
||||
PriceImpact.Type.HIGH -> TangemTheme.colors.text.warning
|
||||
PriceImpact.Type.MEDIUM -> TangemTheme.colors.text.attention
|
||||
else -> TangemTheme.colors.text.tertiary
|
||||
},
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
} else {
|
||||
getFormattedFiatAmount(quoteModel.toTokenInfo.amountFiat)
|
||||
},
|
||||
token = toCurrencyStatus,
|
||||
tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl,
|
||||
coinId = toCurrencyStatus.currency.network.backendId,
|
||||
|
|
@ -436,7 +436,7 @@ internal class StateBuilder(
|
|||
),
|
||||
changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible),
|
||||
providerState = swapProvider.convertToContentClickableProviderState(
|
||||
isBestRate = bestRatedProviderId == swapProvider.providerId,
|
||||
isBestRate = bestRatedProviderId == swapProvider.providerId && !priceImpact.shouldShowWarning(),
|
||||
fromTokenInfo = quoteModel.fromTokenInfo,
|
||||
toTokenInfo = quoteModel.toTokenInfo,
|
||||
isNeedBestRateBadge = isNeedBestRateBadge,
|
||||
|
|
@ -444,7 +444,7 @@ internal class StateBuilder(
|
|||
onProviderClick = actions.onProviderClick,
|
||||
needApplyFCARestrictions = needApplyFCARestrictions,
|
||||
),
|
||||
priceImpact = quoteModel.priceImpact,
|
||||
priceImpact = priceImpact,
|
||||
tosState = createTosState(swapProvider),
|
||||
shouldShowMaxAmount = shouldShowMaxAmount(fromToken, toCurrencyStatus.currency),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue