From e7cd5f4c2efbe4295a7b5ec9874d39a059089864 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 30 Nov 2023 11:42:17 +0300 Subject: [PATCH 1/3] Updated on 2026-08-14 --- .../feature/swap/ui/SwapSelectTokenScreen.kt | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index cde932bf0d..9d90e20505 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.ui +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -11,7 +12,10 @@ import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.common.Strings import com.tangem.core.ui.components.* @@ -34,7 +38,12 @@ fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) .systemBarsPadding() .background(color = TangemTheme.colors.background.secondary), content = { padding -> - ListOfTokens(state = state, Modifier.padding(padding)) + val modifier = Modifier.padding(padding) + if (state.availableTokens.isEmpty() && state.unavailableTokens.isEmpty()) { + EmptyTokensList(modifier) + } else { + ListOfTokens(state = state, modifier = modifier) + } }, topBar = { ExpandableSearchView( @@ -49,6 +58,36 @@ fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) ) } +@Composable +private fun EmptyTokensList(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .background(TangemTheme.colors.background.secondary) + .fillMaxSize() + ) { + Column(modifier = Modifier.align(Alignment.Center)) { + Image( + modifier = Modifier + .size(TangemTheme.dimens.size64) + .align(Alignment.CenterHorizontally), + painter = painterResource(id = R.drawable.ic_no_token_44), + colorFilter = ColorFilter.tint(TangemTheme.colors.icon.inactive), + contentDescription = null, + ) + Text( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing16) + .padding(horizontal = TangemTheme.dimens.spacing30) + .align(Alignment.CenterHorizontally), + text = stringResource(id = R.string.exchange_tokens_empty_tokens), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center + ) + } + } +} + @Composable private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = Modifier) { val screenBackgroundColor = TangemTheme.colors.background.secondary @@ -246,4 +285,12 @@ private fun TokenScreenPreview() { onBack = {}, ) } +} + +@Preview +@Composable +private fun EmptyTokensListPreview() { + TangemTheme(isDark = false) { + EmptyTokensList() + } } \ No newline at end of file From 84390ebc91b8a5608401f7592f128cf1826ec184 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 30 Nov 2023 17:10:11 +0300 Subject: [PATCH 2/3] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractorImpl.kt | 9 +- .../swap/ui/ChooseProviderBottomSheet.kt | 16 ++-- .../tangem/feature/swap/ui/ProviderItem.kt | 2 +- .../tangem/feature/swap/ui/StateBuilder.kt | 40 ++++---- .../feature/swap/ui/SwapSelectTokenScreen.kt | 4 +- .../feature/swap/viewmodels/SwapViewModel.kt | 91 +++++++++++++------ 6 files changed, 99 insertions(+), 63 deletions(-) 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 31615ff878..5a67ab7227 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 @@ -302,7 +302,7 @@ internal class SwapInteractorImpl @Inject constructor( val fromTokenAddress = getTokenAddress(fromToken.currency) val isAllowedToSpend = quotes.dataModel?.allowanceContract?.let { isAllowedToSpend(networkId, fromToken.currency, amount, it) - } ?: false + } ?: true if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) { allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) @@ -543,19 +543,18 @@ internal class SwapInteractorImpl @Inject constructor( value = fee.feeValue, currencySymbol = fee.cryptoSymbol, decimals = fee.decimals, - type = AmountType.Coin + type = AmountType.Coin, ) return if (fee.gasLimit != 0) { Fee.Ethereum( amount = feeAmount, gasLimit = fee.gasLimit.toBigInteger(), - gasPrice = (feeAmountValue / fee.gasLimit.toBigDecimal()).toBigInteger() + gasPrice = (feeAmountValue / fee.gasLimit.toBigDecimal()).toBigInteger(), ) } else { Fee.Common(feeAmount) } - } @Deprecated("used in old swap mechanism") @@ -1083,7 +1082,7 @@ internal class SwapInteractorImpl @Inject constructor( val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI( amount = feeNormal, - decimals = decimals + decimals = decimals, ) val priorityCryptoFee = amountFormatter.formatBigDecimalAmountToUI( amount = feePriority, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt index f11e395923..30954893a7 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt @@ -2,7 +2,6 @@ package com.tangem.feature.swap.ui import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material.Text @@ -24,16 +23,17 @@ import kotlinx.collections.immutable.toImmutableList @Composable fun ChooseProviderBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet(config) { content: ChooseProviderBottomSheetConfig -> + TangemBottomSheet( + config = config, + color = TangemTheme.colors.background.tertiary, + ) { content: ChooseProviderBottomSheetConfig -> ChooseProviderBottomSheetContent(content = content) } } @Composable private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetConfig) { - Column( - modifier = Modifier.background(TangemTheme.colors.background.primary), - ) { + Column { Text( text = stringResource(R.string.express_choose_providers_title), style = TangemTheme.typography.subtitle1, @@ -60,7 +60,6 @@ private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetC shape = TangemTheme.shapes.roundedCornersXMedium, ) .clip(shape = TangemTheme.shapes.roundedCornersXMedium), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { content.providers.forEach { provider -> val isSelected = provider.id == content.selectedProviderId @@ -72,10 +71,7 @@ private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetC enabled = provider.onProviderClick != null, onClick = { provider.onProviderClick?.invoke(provider.id) }, ) - .padding( - horizontal = TangemTheme.dimens.spacing12, - vertical = TangemTheme.dimens.spacing12, - ), + .padding(TangemTheme.dimens.spacing12), ) } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 99595d6398..550fbfcad6 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -146,7 +146,7 @@ private fun ProviderContentState( ) if (state.percentLowerThenBest != null) { Text( - text = "${state.percentLowerThenBest}%", + text = "-${state.percentLowerThenBest}%", style = TangemTheme.typography.body2, color = TangemTheme.colors.text.warning, modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index a13a37110c..0b18d866ad 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -194,12 +194,13 @@ internal class StateBuilder( * @param fromToken token data to swap * @return updated whole screen state */ - @Suppress("LongMethod") + @Suppress("LongMethod", "LongParameterList") fun createQuotesLoadedState( uiStateHolder: SwapStateHolder, quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, swapProvider: SwapProvider, + bestRatedProviderId: String, selectedFeeType: FeeType, ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder @@ -288,6 +289,7 @@ internal class StateBuilder( ), updateInProgress = false, providerState = swapProvider.convertToContentClickableProviderState( + isBestRate = bestRatedProviderId == swapProvider.providerId, fromTokenInfo = quoteModel.fromTokenInfo, toTokenInfo = quoteModel.toTokenInfo, selectionType = ProviderState.SelectionType.CLICK, @@ -310,6 +312,7 @@ internal class StateBuilder( swapProvider = swapProvider, fromToken = fromToken.cryptoCurrencyStatus.currency, dataError = dataError, + onProviderClick = actions.onProviderClick, selectionType = ProviderState.SelectionType.CLICK, ) val receiveCardData = toToken?.let { @@ -358,6 +361,7 @@ internal class StateBuilder( swapProvider: SwapProvider, fromToken: CryptoCurrency, dataError: DataError, + onProviderClick: (String) -> Unit, selectionType: ProviderState.SelectionType, ): ProviderState { return when (dataError) { @@ -368,7 +372,7 @@ internal class StateBuilder( wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)), ), selectionType = selectionType, - onProviderClick = actions.onProviderClick, + onProviderClick = onProviderClick, ) } else -> { @@ -712,13 +716,13 @@ internal class StateBuilder( fun showSelectProviderBottomSheet( uiState: SwapStateHolder, selectedProviderId: String, - bestRatedProviderId: String, + pricesLowerBest: Map, providersStates: Map, unavailableProviders: List, onDismiss: () -> Unit, ): SwapStateHolder { val availableProvidersStates = providersStates.entries.mapNotNull { - it.convertToProviderState(bestRatedProviderId, actions.onProviderSelect) + it.convertToProviderBottomSheetState(pricesLowerBest, actions.onProviderSelect) } val unavailableProviderStates = unavailableProviders.map { it.convertToUnavailableProviderState( @@ -818,23 +822,25 @@ internal class StateBuilder( ).toImmutableList() } - private fun Map.Entry.convertToProviderState( - bestRatedProviderId: String, + private fun Map.Entry.convertToProviderBottomSheetState( + pricesLowerBest: Map, onProviderSelect: (String) -> Unit, ): ProviderState? { val provider = this.key return when (val state = this.value) { is SwapState.EmptyAmountState -> null is SwapState.QuotesLoadedState -> provider.convertToContentSelectableProviderState( - isBestRate = provider.providerId == bestRatedProviderId, + isBestRate = false, // not show best rate in bottom sheet state = state, onProviderClick = onProviderSelect, + pricesLowerBest = pricesLowerBest, selectionType = ProviderState.SelectionType.SELECT, ) is SwapState.SwapError -> getProviderStateForError( swapProvider = provider, fromToken = state.fromTokenInfo.cryptoCurrencyStatus.currency, dataError = state.error, + onProviderClick = onProviderSelect, selectionType = ProviderState.SelectionType.SELECT, ) } @@ -893,6 +899,7 @@ internal class StateBuilder( } private fun SwapProvider.convertToContentClickableProviderState( + isBestRate: Boolean, fromTokenInfo: TokenSwapInfo, toTokenInfo: TokenSwapInfo, selectionType: ProviderState.SelectionType, @@ -905,13 +912,18 @@ internal class StateBuilder( val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol val rateString = "1 $fromCurrencySymbol ≈ $rate $toCurrencySymbol" + val badge = if (isBestRate) { + ProviderState.AdditionalBadge.BestTrade + } else { + ProviderState.AdditionalBadge.Empty + } return ProviderState.Content( id = this.providerId, name = this.name, iconUrl = this.imageLarge, type = this.type.toString(), rate = rateString, - additionalBadge = ProviderState.AdditionalBadge.BestTrade, + additionalBadge = badge, selectionType = selectionType, percentLowerThenBest = null, onProviderClick = onProviderClick, @@ -922,17 +934,11 @@ internal class StateBuilder( isBestRate: Boolean, state: SwapState.QuotesLoadedState, selectionType: ProviderState.SelectionType, + pricesLowerBest: Map, onProviderClick: (String) -> Unit, ): ProviderState { - val fromTokenInfo = state.fromTokenInfo val toTokenInfo = state.toTokenInfo - val rate = toTokenInfo.tokenAmount.value.calculateRate( - fromTokenInfo.tokenAmount.value, - toTokenInfo.cryptoCurrencyStatus.currency.decimals, - ) - val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol - val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol - val rateString = "1 $fromCurrencySymbol ≈ $rate $toCurrencySymbol" + val rateString = toTokenInfo.tokenAmount.getFormattedCryptoAmount(toTokenInfo.cryptoCurrencyStatus.currency) val additionalBadge = if (state.permissionState is PermissionDataState.PermissionReadyForRequest) { ProviderState.AdditionalBadge.PermissionRequired } else if (isBestRate) { @@ -948,7 +954,7 @@ internal class StateBuilder( rate = rateString, additionalBadge = additionalBadge, selectionType = selectionType, - percentLowerThenBest = null, + percentLowerThenBest = pricesLowerBest[this], onProviderClick = onProviderClick, ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 9d90e20505..e1e6a80d73 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -63,7 +63,7 @@ private fun EmptyTokensList(modifier: Modifier = Modifier) { Box( modifier = modifier .background(TangemTheme.colors.background.secondary) - .fillMaxSize() + .fillMaxSize(), ) { Column(modifier = Modifier.align(Alignment.Center)) { Image( @@ -82,7 +82,7 @@ private fun EmptyTokensList(modifier: Modifier = Modifier) { text = stringResource(id = R.string.exchange_tokens_empty_tokens), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index d1dcdef1c0..9304f38196 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.* import arrow.core.getOrElse +import arrow.core.mapNotNull import com.tangem.common.Provider import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.utils.InputNumberFormatter @@ -42,6 +43,8 @@ import java.util.Locale import javax.inject.Inject import kotlin.properties.Delegates +typealias SuccessLoadedSwapData = Map + @Suppress("LargeClass", "LongParameterList") @HiltViewModel internal class SwapViewModel @Inject constructor( @@ -257,8 +260,12 @@ internal class SwapViewModel @Inject constructor( } }, onSuccess = { providersState -> - val (provider, state) = updateLoadedQuotes(providersState) - setupLoadedState(provider, state, fromToken) + if (providersState.isNotEmpty()) { + val (provider, state) = updateLoadedQuotes(providersState) + setupLoadedState(provider, state, fromToken) + } else { + Timber.e("Accidentally empty quotes list") + } }, onError = { Timber.e("Error when loading quotes: $it") @@ -271,11 +278,14 @@ internal class SwapViewModel @Inject constructor( when (state) { is SwapState.QuotesLoadedState -> { fillLoadedDataState(state, state.permissionState, state.swapDataModel) + val loadedStates = dataState.lastLoadedSwapStates.getLastLoadedSuccessStates() + val bestRatedProviderId = findBestQuoteProvider(loadedStates)?.providerId ?: provider.providerId uiState = stateBuilder.createQuotesLoadedState( uiStateHolder = uiState, quoteModel = state, fromToken = fromToken.currency, swapProvider = provider, + bestRatedProviderId = bestRatedProviderId, selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL, ) } @@ -309,15 +319,13 @@ internal class SwapViewModel @Inject constructor( lastLoadedSwapStates = state, ) selectedSwapProvider?.let { - return state.entries.first { it.key == selectedSwapProvider }.toPair() + return nonEmptyStates.entries.first { it.key == selectedSwapProvider }.toPair() } return state.entries.first().toPair() } private fun selectProvider(state: Map): SwapProvider { - val stateSuccess = state - .filter { it.value is SwapState.QuotesLoadedState } - .mapValues { it.value as SwapState.QuotesLoadedState } + val stateSuccess = state.getLastLoadedSuccessStates() return if (stateSuccess.isNotEmpty()) { val currentSelected = dataState.selectedProvider if (currentSelected != null && state.keys.contains(currentSelected)) { @@ -330,23 +338,6 @@ internal class SwapViewModel @Inject constructor( } } - private fun findBestQuoteProvider(state: Map): SwapProvider? { - // finding best quotes - return state.mapValues { - if (!it.value.fromTokenInfo.amountFiat.isNullOrZero() && - !it.value.toTokenInfo.amountFiat.isNullOrZero() - ) { - it.value.fromTokenInfo.amountFiat.divide( - it.value.toTokenInfo.amountFiat, - it.value.toTokenInfo.cryptoCurrencyStatus.currency.decimals, - RoundingMode.HALF_UP, - ) - } else { - BigDecimal.ZERO - } - }.minByOrNull { it.value }?.key - } - private fun fillLoadedDataState( state: SwapState.QuotesLoadedState, permissionState: PermissionDataState, @@ -714,15 +705,13 @@ internal class SwapViewModel @Inject constructor( } }, onProviderClick = { providerId -> - val states = dataState.lastLoadedSwapStates - .filter { it.value is SwapState.QuotesLoadedState } - .mapValues { it.value as SwapState.QuotesLoadedState } - val bestRatedProviderId = findBestQuoteProvider(states)?.providerId ?: providerId + val states = dataState.lastLoadedSwapStates.getLastLoadedSuccessStates() + val pricesLowerBest = getPricesLowerBest(states) val unavailableProviders = getUnavailableProvidersFor(dataState.lastLoadedSwapStates) uiState = stateBuilder.showSelectProviderBottomSheet( uiState = uiState, selectedProviderId = providerId, - bestRatedProviderId = bestRatedProviderId, + pricesLowerBest = pricesLowerBest, unavailableProviders = unavailableProviders, providersStates = dataState.lastLoadedSwapStates, ) { uiState = stateBuilder.dismissBottomSheet(uiState) } @@ -754,6 +743,45 @@ internal class SwapViewModel @Inject constructor( return selectedProvider } + private fun findBestQuoteProvider(state: SuccessLoadedSwapData): SwapProvider? { + // finding best quotes + return state.minByOrNull { + if (!it.value.fromTokenInfo.amountFiat.isNullOrZero() && + !it.value.toTokenInfo.amountFiat.isNullOrZero() + ) { + it.value.fromTokenInfo.amountFiat.divide( + it.value.toTokenInfo.amountFiat, + it.value.toTokenInfo.cryptoCurrencyStatus.currency.decimals, + RoundingMode.HALF_UP, + ) + } else { + BigDecimal.ZERO + } + }?.key + } + + private fun getPricesLowerBest(state: SuccessLoadedSwapData): Map { + val rates = state.mapValues { + it.value.fromTokenInfo.amountFiat.divide( + it.value.toTokenInfo.amountFiat, + 2, + RoundingMode.HALF_UP, + ) + } + val bestRate = rates.minByOrNull { it.value } ?: return emptyMap() + return rates.mapNotNull { + if (it.key != bestRate.key) { + val percentDiff = bestRate.value + .divide(it.value, 2, RoundingMode.HALF_UP) + .multiply(BigDecimal(HUNDRED_PERCENT)) + .toFloat() + HUNDRED_PERCENT - percentDiff + } else { + null + } + } + } + private fun createSelectedAppCurrencyFlow(): StateFlow { return getSelectedAppCurrencyUseCase() .map { maybeAppCurrency -> @@ -794,10 +822,17 @@ internal class SwapViewModel @Inject constructor( return getAllProviders().filterNot { it in state } } + private fun Map.getLastLoadedSuccessStates(): SuccessLoadedSwapData { + return this + .filter { it.value is SwapState.QuotesLoadedState } + .mapValues { it.value as SwapState.QuotesLoadedState } + } + companion object { private const val loggingTag = "SwapViewModel" private const val INITIAL_AMOUNT = "" private const val UPDATE_DELAY = 10000L private const val DEBOUNCE_AMOUNT_DELAY = 1000L + private const val HUNDRED_PERCENT = 100 } } \ No newline at end of file From 4c23520e0851b819885f76e16a6e5b91403c26c2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 30 Nov 2023 17:26:29 +0300 Subject: [PATCH 3/3] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- .../feature/swap/domain/SwapInteractor.kt | 13 +-- .../feature/swap/domain/SwapInteractorImpl.kt | 92 ++++--------------- .../swap/domain/cache/SwapDataCache.kt | 15 --- .../swap/domain/cache/SwapDataCacheImpl.kt | 41 --------- .../swap/domain/di/SwapDomainModule.kt | 2 - .../feature/swap/viewmodels/SwapViewModel.kt | 2 +- 7 files changed, 21 insertions(+), 146 deletions(-) delete mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt delete mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 2a612cd92b..6ef8ce45d1 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 2a612cd92b1c78b99c917d0fe66342831f801e5e +Subproject commit 6ef8ce45d183905b5752e2d33c1d8bf2f4bcace6 diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 6f550682f4..01193a0ea4 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -14,16 +14,6 @@ interface SwapInteractor { fun initDerivationPathAndNetwork(derivationPath: String?, network: Network) - /** - * On search token, locally search tokens in previously loaded list to swap - * searching in names and symbols - * - * @param networkId networkId for tokens - * @param searchQuery string query for search - * @return [FoundTokensStateExpress] that contains list of tokens matching condition query - */ - suspend fun searchTokens(networkId: String, searchQuery: String): FoundTokensStateExpress - /** * Gives permission to swap, this starts scan card process * @@ -88,10 +78,9 @@ interface SwapInteractor { /** * Returns token in wallet balance * - * @param networkId * @param token */ - fun getTokenBalance(networkId: String, token: CryptoCurrency): SwapAmount + fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount fun isAvailableToSwap(networkId: String): Boolean 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 5a67ab7227..a97c761008 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 @@ -19,7 +19,6 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.feature.swap.domain.cache.SwapDataCache import com.tangem.feature.swap.domain.converters.SwapCurrencyConverter import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel @@ -44,7 +43,6 @@ internal class SwapInteractorImpl @Inject constructor( private val transactionManager: TransactionManager, private val userWalletManager: UserWalletManager, private val repository: SwapRepository, - private val cache: SwapDataCache, private val allowPermissionsHandler: AllowPermissionsHandler, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, @@ -166,25 +164,6 @@ internal class SwapInteractorImpl @Inject constructor( this.network = network } - @Deprecated("used in old swap mechanism") - override suspend fun searchTokens(networkId: String, searchQuery: String): FoundTokensStateExpress { - val searchQueryLowerCase = searchQuery.lowercase() - val tokensInWallet = cache.getInWalletTokens() - .filter { - it.token.name.lowercase().contains(searchQueryLowerCase) || - it.token.symbol.lowercase().contains(searchQueryLowerCase) - } - val loadedTokens = cache.getLoadedTokens() - .filter { - it.token.name.lowercase().contains(searchQueryLowerCase) || - it.token.symbol.lowercase().contains(searchQueryLowerCase) - } - return FoundTokensStateExpress( - tokensInWallet = tokensInWallet, - loadedTokens = loadedTokens, - ) - } - @Deprecated("used in old swap mechanism") override suspend fun givePermissionToSwap(networkId: String, permissionOptions: PermissionOptions): TxState { val dataToSign = if (permissionOptions.approveType == SwapApproveType.UNLIMITED) { @@ -238,19 +217,14 @@ internal class SwapInteractorImpl @Inject constructor( selectedFee: FeeType, ): Map { return providers.map { provider -> - syncWalletBalanceForTokens(networkId, listOf(fromToken.currency, toToken.currency)) val amountDecimal = toBigDecimalOrNull(amountToSwap) if (amountDecimal == null || amountDecimal.signum() == 0) { return providers.associateWith { - createEmptyAmountState( - networkId, - fromToken.currency, - toToken.currency, - ) + createEmptyAmountState(fromToken, toToken) } } val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken.currency)) - val isBalanceWithoutFeeEnough = isBalanceEnough(networkId, fromToken.currency, amount, null) + val isBalanceWithoutFeeEnough = isBalanceEnough(fromToken, amount, null) when (provider.type) { ExchangeProviderType.DEX -> { @@ -406,7 +380,7 @@ internal class SwapInteractorImpl @Inject constructor( val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken.currency)) val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = state.txFee) val isBalanceIncludeFeeEnough = - isBalanceEnough(networkId, fromToken.currency, amount, feeByPriority) + isBalanceEnough(fromToken, amount, feeByPriority) val isFeeEnough = checkFeeIsEnough( fee = feeByPriority, spendAmount = amount, @@ -558,12 +532,8 @@ internal class SwapInteractorImpl @Inject constructor( } @Deprecated("used in old swap mechanism") - override fun getTokenBalance(networkId: String, token: CryptoCurrency): SwapAmount { - return cache.getBalanceForToken( - networkId = networkId, - derivationPath = derivationPath, - symbol = token.symbol, - ) ?: SwapAmount(BigDecimal.ZERO, getTokenDecimals(token)) + override fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount { + return SwapAmount(token.value.amount ?: BigDecimal.ZERO, getTokenDecimals(token.currency)) } @Deprecated("used in old swap mechanism") @@ -610,17 +580,13 @@ internal class SwapInteractorImpl @Inject constructor( ) } - private fun createEmptyAmountState( - networkId: String, - fromToken: CryptoCurrency, - toToken: CryptoCurrency, - ): SwapState { + private fun createEmptyAmountState(fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus): SwapState { val appCurrency = userWalletManager.getUserAppCurrency() - val fromTokenBalance = cache.getBalanceForToken(networkId, derivationPath, fromToken.symbol) - val toTokenBalance = cache.getBalanceForToken(networkId, derivationPath, toToken.symbol) + val fromTokenBalance = getTokenBalance(fromToken) + val toTokenBalance = getTokenBalance(toToken) return SwapState.EmptyAmountState( - fromTokenWalletBalance = fromTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") }.orEmpty(), - toTokenWalletBalance = toTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") }.orEmpty(), + fromTokenWalletBalance = amountFormatter.formatSwapAmountToUI(fromTokenBalance, ""), + toTokenWalletBalance = amountFormatter.formatSwapAmountToUI(toTokenBalance, ""), zeroAmountEquivalent = BigDecimal.ZERO.toFiatString( rateValue = BigDecimal.ONE, fiatCurrencyName = appCurrency.symbol, @@ -706,7 +672,7 @@ internal class SwapInteractorImpl @Inject constructor( ExchangeProviderType.DEX -> { val state = updatePermissionState( networkId = networkId, - fromToken = fromToken.currency, + fromTokenStatus = fromToken, swapAmount = amount, quotesLoadedState = swapState, isAllowedToSpend = isAllowedToSpend, @@ -799,8 +765,7 @@ internal class SwapInteractorImpl @Inject constructor( is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(networkId) } val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = txFeeState) - val isBalanceIncludeFeeEnough = - isBalanceEnough(networkId, fromToken.currency, amount, feeByPriority) + val isBalanceIncludeFeeEnough = isBalanceEnough(fromToken, amount, feeByPriority) val isFeeEnough = checkFeeIsEnough( fee = feeByPriority, spendAmount = amount, @@ -908,19 +873,20 @@ internal class SwapInteractorImpl @Inject constructor( @Suppress("LongParameterList", "LongMethod") private suspend fun updatePermissionState( networkId: String, - fromToken: CryptoCurrency, + fromTokenStatus: CryptoCurrencyStatus, swapAmount: SwapAmount, quotesLoadedState: SwapState.QuotesLoadedState, spenderAddress: String, isAllowedToSpend: Boolean, ): SwapState.QuotesLoadedState { + val fromToken = fromTokenStatus.currency if (isAllowedToSpend) { return quotesLoadedState.copy( permissionState = PermissionDataState.Empty, ) } // if token balance ZERO not show permission state to avoid user to spend money for fee - val isTokenZeroBalance = getTokenBalance(networkId, fromToken).value.signum() == 0 + val isTokenZeroBalance = getTokenBalance(fromTokenStatus).value.signum() == 0 if (isTokenZeroBalance) { return quotesLoadedState.copy( permissionState = PermissionDataState.Empty, @@ -989,23 +955,6 @@ internal class SwapInteractorImpl @Inject constructor( ) } - private suspend fun syncWalletBalanceForTokens(networkId: String, tokens: List) { - val tokensToSync = tokens.filter { cache.getBalanceForToken(networkId, derivationPath, it.symbol) == null } - if (tokensToSync.isNotEmpty()) { - val tokensBalance = - userWalletManager.getCurrentWalletTokensBalance( - networkId = networkId, - extraTokens = tokensToSync.map { swapCurrencyConverter.convert(it) }, - derivationPath = derivationPath, - ) - cache.cacheBalances( - networkId = networkId, - derivationPath = derivationPath, - balances = tokensBalance.mapValues { SwapAmount(it.value.value, it.value.decimals) }, - ) - } - } - private suspend fun ProxyFees.MultipleFees.proxyFeesToFeeState(networkId: String): TxFeeState { val normalFeeValue = this.minFee.fee.value // in swap for normal use min fee val normalFeeGas = this.minFee.gasLimit.toInt() @@ -1149,14 +1098,9 @@ internal class SwapInteractorImpl @Inject constructor( } } - private fun isBalanceEnough( - networkId: String, - fromToken: CryptoCurrency, - amount: SwapAmount, - fee: BigDecimal?, - ): Boolean { - val tokenBalance = getTokenBalance(networkId, fromToken).value - return if (fromToken is CryptoCurrency.Token) { + private fun isBalanceEnough(fromToken: CryptoCurrencyStatus, amount: SwapAmount, fee: BigDecimal?): Boolean { + val tokenBalance = getTokenBalance(fromToken).value + return if (fromToken.currency is CryptoCurrency.Token) { tokenBalance >= amount.value } else { tokenBalance > amount.value.plus(fee ?: BigDecimal.ZERO) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt deleted file mode 100644 index ce4956ba90..0000000000 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.feature.swap.domain.cache - -import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.ui.TokenWithBalanceExpress - -interface SwapDataCache { - - fun cacheInWalletTokens(tokens: List) - fun cacheLoadedTokens(tokens: List) - fun cacheBalances(networkId: String, derivationPath: String?, balances: Map) - - fun getInWalletTokens(): List - fun getLoadedTokens(): List - fun getBalanceForToken(networkId: String, derivationPath: String?, symbol: String): SwapAmount? -} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt deleted file mode 100644 index 92c051e246..0000000000 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.tangem.feature.swap.domain.cache - -import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.ui.TokenWithBalanceExpress - -class SwapDataCacheImpl : SwapDataCache { - - private val tokensBalances: MutableMap> = mutableMapOf() - private val lastInWalletTokens = mutableListOf() - private val lastLoadedTokens = mutableListOf() - - override fun cacheInWalletTokens(tokens: List) { - lastInWalletTokens.clear() - lastInWalletTokens.addAll(tokens) - } - - override fun cacheLoadedTokens(tokens: List) { - lastLoadedTokens.clear() - lastLoadedTokens.addAll(tokens) - } - - override fun getInWalletTokens(): List { - return lastInWalletTokens - } - - override fun getLoadedTokens(): List { - return lastLoadedTokens - } - - override fun getBalanceForToken(networkId: String, derivationPath: String?, symbol: String): SwapAmount? { - return tokensBalances[createKeyFrom(networkId, derivationPath)]?.get(symbol) - } - - override fun cacheBalances(networkId: String, derivationPath: String?, balances: Map) { - tokensBalances[createKeyFrom(networkId, derivationPath)] = balances - } - - private fun createKeyFrom(networkId: String, derivationPath: String?): String { - return "$networkId;$derivationPath" - } -} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index dd81f44d66..e161ee2429 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -13,7 +13,6 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.swap.domain.* -import com.tangem.feature.swap.domain.cache.SwapDataCacheImpl import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -45,7 +44,6 @@ class SwapDomainModule { transactionManager = transactionManager, userWalletManager = userWalletManager, repository = swapRepository, - cache = SwapDataCacheImpl(), allowPermissionsHandler = AllowPermissionsHandlerImpl(), getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, getMultiCryptoCurrencyStatusUseCase = getCryptoCurrencyStatusUseCase, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 9304f38196..4122723684 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -592,7 +592,7 @@ internal class SwapViewModel @Inject constructor( private fun onMaxAmountClicked() { dataState.fromCryptoCurrency?.let { - val balance = swapInteractor.getTokenBalance(initialCryptoCurrency.network.id.value, it.currency) + val balance = swapInteractor.getTokenBalance(it) onAmountChanged(balance.formatToUIRepresentation()) } }