diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt index 0c883b505c..a3191300f9 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt @@ -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 { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt index c0d35dcc2d..fb20d25a67 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt @@ -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 diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 32031b0b71..e44392c352 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -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 = "∞" diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index 89a8266fd1..e0fccb451d 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -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, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index b3d2170b7f..dcdc0a63a5 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -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, ) } },