Updated on 2026-08-14
This commit is contained in:
commit
82c19782eb
11 changed files with 166 additions and 208 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit 2a612cd92b1c78b99c917d0fe66342831f801e5e
|
||||
Subproject commit 6ef8ce45d183905b5752e2d33c1d8bf2f4bcace6
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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<SwapProvider, SwapState> {
|
||||
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 -> {
|
||||
|
|
@ -302,7 +276,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)
|
||||
|
|
@ -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,
|
||||
|
|
@ -543,28 +517,23 @@ 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")
|
||||
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")
|
||||
|
|
@ -611,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,
|
||||
|
|
@ -707,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,
|
||||
|
|
@ -800,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,
|
||||
|
|
@ -909,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,
|
||||
|
|
@ -990,23 +955,6 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun syncWalletBalanceForTokens(networkId: String, tokens: List<CryptoCurrency>) {
|
||||
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()
|
||||
|
|
@ -1083,7 +1031,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
|
||||
val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI(
|
||||
amount = feeNormal,
|
||||
decimals = decimals
|
||||
decimals = decimals,
|
||||
)
|
||||
val priorityCryptoFee = amountFormatter.formatBigDecimalAmountToUI(
|
||||
amount = feePriority,
|
||||
|
|
@ -1150,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)
|
||||
|
|
|
|||
|
|
@ -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<TokenWithBalanceExpress>)
|
||||
fun cacheLoadedTokens(tokens: List<TokenWithBalanceExpress>)
|
||||
fun cacheBalances(networkId: String, derivationPath: String?, balances: Map<String, SwapAmount>)
|
||||
|
||||
fun getInWalletTokens(): List<TokenWithBalanceExpress>
|
||||
fun getLoadedTokens(): List<TokenWithBalanceExpress>
|
||||
fun getBalanceForToken(networkId: String, derivationPath: String?, symbol: String): SwapAmount?
|
||||
}
|
||||
|
|
@ -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<String, Map<String, SwapAmount>> = mutableMapOf()
|
||||
private val lastInWalletTokens = mutableListOf<TokenWithBalanceExpress>()
|
||||
private val lastLoadedTokens = mutableListOf<TokenWithBalanceExpress>()
|
||||
|
||||
override fun cacheInWalletTokens(tokens: List<TokenWithBalanceExpress>) {
|
||||
lastInWalletTokens.clear()
|
||||
lastInWalletTokens.addAll(tokens)
|
||||
}
|
||||
|
||||
override fun cacheLoadedTokens(tokens: List<TokenWithBalanceExpress>) {
|
||||
lastLoadedTokens.clear()
|
||||
lastLoadedTokens.addAll(tokens)
|
||||
}
|
||||
|
||||
override fun getInWalletTokens(): List<TokenWithBalanceExpress> {
|
||||
return lastInWalletTokens
|
||||
}
|
||||
|
||||
override fun getLoadedTokens(): List<TokenWithBalanceExpress> {
|
||||
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<String, SwapAmount>) {
|
||||
tokensBalances[createKeyFrom(networkId, derivationPath)] = balances
|
||||
}
|
||||
|
||||
private fun createKeyFrom(networkId: String, derivationPath: String?): String {
|
||||
return "$networkId;$derivationPath"
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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<SwapProvider, Float>,
|
||||
providersStates: Map<SwapProvider, SwapState>,
|
||||
unavailableProviders: List<SwapProvider>,
|
||||
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<SwapProvider, SwapState>.convertToProviderState(
|
||||
bestRatedProviderId: String,
|
||||
private fun Map.Entry<SwapProvider, SwapState>.convertToProviderBottomSheetState(
|
||||
pricesLowerBest: Map<SwapProvider, Float>,
|
||||
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<SwapProvider, Float>,
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SwapProvider, SwapState.QuotesLoadedState>
|
||||
|
||||
@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, SwapState>): 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, SwapState.QuotesLoadedState>): 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,
|
||||
|
|
@ -601,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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SwapProvider, Float> {
|
||||
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<AppCurrency> {
|
||||
return getSelectedAppCurrencyUseCase()
|
||||
.map { maybeAppCurrency ->
|
||||
|
|
@ -794,10 +822,17 @@ internal class SwapViewModel @Inject constructor(
|
|||
return getAllProviders().filterNot { it in state }
|
||||
}
|
||||
|
||||
private fun Map<SwapProvider, SwapState>.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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue