Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-08 13:31:42 +05:00
parent 94579f4af8
commit f69ee8bece
5 changed files with 98 additions and 21 deletions

View file

@ -23,6 +23,7 @@ internal object SwapAmountQuoteUtils {
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_AMOUNT_LOW_THRESHOLD = 100_000.toBigDecimal() // in USD
private val PRICE_IMPACT_LOW_THRESHOLD = 0.1.toBigDecimal() // 10%
private val PRICE_IMPACT_HIGH_THRESHOLD = 0.5.toBigDecimal() // 50%
@ -46,10 +47,10 @@ internal object SwapAmountQuoteUtils {
secondaryCryptoCurrencyStatus?.value?.fiatRate to primaryCryptoCurrencyStatus.value.fiatRate
}
val fromRateUsd = if (swapDirection == SwapDirection.Direct) {
primaryFiatRateUSD
val (fromRateUsd, toRateUsd) = if (swapDirection == SwapDirection.Direct) {
primaryFiatRateUSD to secondaryFiatRateUSD
} else {
secondaryFiatRateUSD
secondaryFiatRateUSD to primaryFiatRateUSD
}
val fromTokenFiatValue = fromRate?.let { fromAmount.multiply(fromRate).orZero() }
@ -65,12 +66,15 @@ internal object SwapAmountQuoteUtils {
}
val fromAmountUSD = fromAmount.multiply(fromRateUsd.orZero())
val toAmountUSD = toAmount.multiply(toRateUsd.orZero())
val amountDiff = fromAmountUSD - toAmountUSD
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
value < PRICE_IMPACT_LOW_THRESHOLD &&
amountDiff <= PRICE_IMPACT_AMOUNT_LOW_THRESHOLD -> PriceImpact.Type.LOW
value > PRICE_IMPACT_HIGH_THRESHOLD -> PriceImpact.Type.HIGH
else -> PriceImpact.Type.MEDIUM
}
val amountSignificance = when {

View file

@ -7,11 +7,11 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_TO
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_MESSAGE
import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE
import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER
import com.tangem.core.analytics.models.AnalyticsParam.Key.RATE_TYPE
import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_BLOCKCHAIN
import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_TOKEN
import com.tangem.core.analytics.models.AnalyticsParam.Key.SEND_BLOCKCHAIN
import com.tangem.core.analytics.models.AnalyticsParam.Key.SEND_TOKEN
import com.tangem.core.analytics.models.AnalyticsParam.Key.RATE_TYPE
import com.tangem.core.analytics.models.AppsFlyerIncludedEvent
import com.tangem.domain.express.models.ExpressRateType
import com.tangem.domain.models.currency.CryptoCurrency

View file

@ -1962,7 +1962,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
fromTokenAmount = fromTokenAmount.value,
fromQuoteStatus = rates[fromToken.id],
toTokenAmount = toTokenAmount.value,
toRate = rates[toToken.id]?.fiatRate,
toQuoteStatus = rates[toToken.id],
),
swapDataModel = swapData,
swapProvider = provider,
@ -2436,10 +2436,11 @@ internal class SwapInteractorImpl @AssistedInject constructor(
fromTokenAmount: BigDecimal,
fromQuoteStatus: QuoteStatus.Data?,
toTokenAmount: BigDecimal,
toRate: BigDecimal?,
toQuoteStatus: QuoteStatus.Data?,
): PriceImpact {
if (fromQuoteStatus == null) return PriceImpact.Empty
val toRate = toQuoteStatus?.fiatRate
val fromTokenFiatValue = fromTokenAmount.multiply(fromQuoteStatus.fiatRate)
val toTokenFiatValue = toRate?.let { toTokenAmount.multiply(toRate) } ?: return PriceImpact.Empty
@ -2450,6 +2451,12 @@ internal class SwapInteractorImpl @AssistedInject constructor(
} else {
PRICE_IMPACT_AMOUNT_MIN_THRESHOLD
}
val toAmountUSD = if (toQuoteStatus.fiatRateUSD != BigDecimal.ZERO) {
toTokenAmount.multiply(toQuoteStatus.fiatRateUSD)
} else {
PRICE_IMPACT_AMOUNT_MIN_THRESHOLD
}
val amountDiff = fromAmountUSD - toAmountUSD
val amountSignificance = when {
fromAmountUSD <= PRICE_IMPACT_AMOUNT_MIN_THRESHOLD -> PriceImpact.AmountSignificance.LOW
@ -2458,9 +2465,10 @@ internal class SwapInteractorImpl @AssistedInject constructor(
}
val type = when {
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
value < PRICE_IMPACT_LOW_THRESHOLD &&
amountDiff <= PRICE_IMPACT_AMOUNT_LOW_THRESHOLD -> PriceImpact.Type.LOW
value > PRICE_IMPACT_HIGH_THRESHOLD -> PriceImpact.Type.HIGH
else -> PriceImpact.Type.MEDIUM
}
return PriceImpact(
@ -2539,6 +2547,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5%
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_AMOUNT_LOW_THRESHOLD = 100_000.toBigDecimal() // in USD
private val PRICE_IMPACT_LOW_THRESHOLD = 0.1.toBigDecimal() // 10%
private val PRICE_IMPACT_HIGH_THRESHOLD = 0.5.toBigDecimal() // 50%
private const val INFINITY_SYMBOL = ""

View file

@ -161,6 +161,41 @@ sealed class SwapEvents(
),
)
class HighPriceImpact(
val sendToken: String,
val receiveToken: String,
val sendBlockchain: String,
val receiveBlockchain: String,
val providerName: String,
) : SwapEvents(
event = "Notice - High price impact",
params = mapOf(
SEND_TOKEN to sendToken,
RECEIVE_TOKEN to receiveToken,
"Send Blockchain" to sendBlockchain,
"Receive Blockchain" to receiveBlockchain,
PROVIDER to providerName,
),
)
class TradeTooLarge(
val sendToken: String,
val receiveToken: String,
val sendBlockchain: String,
val receiveBlockchain: String,
val providerName: String,
) : SwapEvents(
event = "Notice - Trade too large",
params = mapOf(
SEND_TOKEN to sendToken,
RECEIVE_TOKEN to receiveToken,
"Send Blockchain" to sendBlockchain,
"Receive Blockchain" to receiveBlockchain,
PROVIDER to providerName,
),
)
class NoticeProviderError(
val sendToken: String,
val receiveToken: String,

View file

@ -82,11 +82,7 @@ import com.tangem.feature.swap.domain.models.ExpressException
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.SwapAlertUM
import com.tangem.feature.swap.models.SwapCardState
import com.tangem.feature.swap.models.SwapStateHolder
import com.tangem.feature.swap.models.TransactionCardType
import com.tangem.feature.swap.models.UiActions
import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.models.states.SwapNotificationUM
import com.tangem.feature.swap.router.SwapNavScreen
import com.tangem.feature.swap.router.SwapRouter
@ -712,7 +708,7 @@ internal class SwapModel @Inject constructor(
onSuccess = { providersState ->
if (providersState.isNotEmpty()) {
val (provider, state) = updateLoadedQuotes(providersState)
setupLoadedState(provider, state, fromToken)
setupLoadedState(provider = provider, state = state, fromToken = fromToken, toToken = toToken)
val successStates = providersState.getLastLoadedSuccessStates()
val pricesLowerBest = getPricesLowerBest(provider.providerId, successStates)
uiState = stateBuilder.updateProvidersBottomSheetContent(
@ -739,11 +735,16 @@ internal class SwapModel @Inject constructor(
)
}
private fun setupLoadedState(provider: SwapProvider, state: SwapState, fromToken: CryptoCurrencyStatus) {
private fun setupLoadedState(
provider: SwapProvider,
state: SwapState,
fromToken: CryptoCurrencyStatus,
toToken: CryptoCurrencyStatus?,
) {
when (state) {
is SwapState.QuotesLoadedState -> {
setupQuotesLoadedUiState(provider, state, fromToken)
sendAnalyticsForNotifications(fromToken)
sendAnalyticsForNotifications(provider, fromToken, toToken)
updatePermissionNotificationState(state)
}
is SwapState.EmptyAmountState -> {
@ -780,7 +781,11 @@ internal class SwapModel @Inject constructor(
)
}
private fun sendAnalyticsForNotifications(fromToken: CryptoCurrencyStatus) {
private fun sendAnalyticsForNotifications(
provider: SwapProvider,
fromToken: CryptoCurrencyStatus,
toToken: CryptoCurrencyStatus?,
) {
if (uiState.notifications.any { it is SwapNotificationUM.Error.UnableToCoverFeeWarning }) {
analyticsEventHandler.send(
SwapEvents.NoticeNotEnoughFee(
@ -789,6 +794,28 @@ internal class SwapModel @Inject constructor(
),
)
}
if (toToken != null && uiState.notifications.any { it is SwapNotificationUM.Warning.HighPriceImpact }) {
analyticsEventHandler.send(
SwapEvents.HighPriceImpact(
sendToken = fromToken.currency.symbol,
receiveToken = toToken.currency.network.name,
sendBlockchain = fromToken.currency.symbol,
receiveBlockchain = toToken.currency.network.name,
providerName = provider.name,
),
)
}
if (toToken != null && uiState.notifications.any { it is SwapNotificationUM.Warning.TradeTooHigh }) {
analyticsEventHandler.send(
SwapEvents.TradeTooLarge(
sendToken = fromToken.currency.symbol,
receiveToken = toToken.currency.network.name,
sendBlockchain = fromToken.currency.symbol,
receiveBlockchain = toToken.currency.network.name,
providerName = provider.name,
),
)
}
}
private fun updatePermissionNotificationState(state: SwapState.QuotesLoadedState) {
@ -1719,6 +1746,7 @@ internal class SwapModel @Inject constructor(
val provider = findAndSelectProvider(providerId)
val swapState = dataState.lastLoadedSwapStates[provider]
val fromToken = dataState.fromCryptoCurrency
val toToken = dataState.toCryptoCurrency
if (provider != null && swapState != null && fromToken != null) {
modelScope.launch {
feeSelectorRepository.state.value = FeeSelectorUM.Loading
@ -1730,6 +1758,7 @@ internal class SwapModel @Inject constructor(
provider = provider,
state = swapState,
fromToken = fromToken,
toToken = toToken,
)
}
},