Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-22 16:27:08 +03:00
parent 64ed64820b
commit a6f272223a
18 changed files with 237 additions and 179 deletions

View file

@ -32,7 +32,7 @@ interface TangemExpressApi {
@Query("toContractAddress") toContractAddress: String,
@Query("toNetwork") toNetwork: String,
@Query("fromAmount") fromAmount: String,
@Query("providerId") providerId: Int,
@Query("providerId") providerId: String,
@Query("rateType") rateType: String,
): ApiResponse<ExchangeQuoteResponse>

View file

@ -4,19 +4,19 @@ import com.squareup.moshi.Json
data class ExchangeProvider(
@Json(name = "id")
val id: Int,
val id: String,
@Json(name = "name")
val name: String,
@Json(name = "id")
@Json(name = "type")
val type: ExchangeProviderType,
@Json(name = "imageLarge")
val imageLargeUrl: Int,
val imageLargeUrl: String,
@Json(name = "imageSmall")
val imageSmallUrl: Int,
val imageSmallUrl: String,
)
enum class ExchangeProviderType {

View file

@ -17,7 +17,7 @@ data class SwapPair(
data class SwapPairProvider(
@Json(name = "providerId")
val providerId: Int,
val providerId: String,
@Json(name = "rateTypes")
val rateTypes: List<RateType>,

View file

@ -48,6 +48,7 @@ internal class SwapRepositoryImpl @Inject constructor(
private val leastTokenInfoConverter = LeastTokenInfoConverter()
private val swapPairInfoConverter = SwapPairInfoConverter()
private val rateTypeConverter = RateTypeConverter()
private val swapProviderConverter = SwapProviderConverter()
override suspend fun getPairs(
initialCurrency: LeastTokenInfo,
@ -78,6 +79,18 @@ internal class SwapRepositoryImpl @Inject constructor(
}
}
override suspend fun getProvidersDetails(providers: Set<SwapProvider>): List<SwapProvider> {
val providersMap = providers.associateBy { it.providerId }
return tangemExpressApi.getProviders().getOrThrow().mapNotNull {
val provider = providersMap[it.id]
if (provider != null) {
swapProviderConverter.convert(it).copy(rateTypes = provider.rateTypes)
} else {
null
}
}
}
private suspend fun getPairsInternal(
from: List<NetworkLeastTokenInfo>,
to: List<NetworkLeastTokenInfo>,
@ -133,7 +146,7 @@ internal class SwapRepositoryImpl @Inject constructor(
toContractAddress: String,
toNetwork: String,
fromAmount: String,
providerId: Int,
providerId: String,
rateType: RateType,
): AggregatedSwapDataModel<QuoteModel> {
return withContext(coroutineDispatcher.io) {
@ -274,7 +287,7 @@ internal class SwapRepositoryImpl @Inject constructor(
toContractAddress: String,
toNetwork: String,
fromAmount: String,
providerId: Int,
providerId: String,
rateType: RateType,
): ExchangeQuote {
val response = tangemExpressApi.getExchangeQuote(

View file

@ -0,0 +1,25 @@
package com.tangem.feature.swap.converters
import com.tangem.datasource.api.express.models.response.ExchangeProvider
import com.tangem.datasource.api.express.models.response.ExchangeProviderType
import com.tangem.feature.swap.domain.models.domain.SwapProvider
import com.tangem.utils.converter.Converter
import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType as ExchangeProviderTypeDomain
class SwapProviderConverter : Converter<ExchangeProvider, SwapProvider> {
override fun convert(value: ExchangeProvider): SwapProvider {
return SwapProvider(
providerId = value.id,
name = value.name,
type = convertExchangeType(value.type),
imageLarge = value.imageLargeUrl,
)
}
private fun convertExchangeType(type: ExchangeProviderType): ExchangeProviderTypeDomain {
return when (type) {
ExchangeProviderType.DEX -> ExchangeProviderTypeDomain.DEX
ExchangeProviderType.CEX -> ExchangeProviderTypeDomain.CEX
}
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.feature.swap.domain
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.swap.domain.models.SwapAmount
@ -14,16 +15,6 @@ interface SwapInteractor {
fun initDerivationPathAndNetwork(derivationPath: String?, network: Network)
/**
* Init tokens to swap, load tokens list available to swap for given network
*
* @param initialCurrency currency which to swap or receive
* @return [TokensDataState] that contains info about all available to swap tokens for networkId
* and preselected tokens which initially select to swap
*/
@Deprecated("method is used in the old swap mechanism")
suspend fun initTokensToSwap(initialCurrency: Currency): TokensDataState
/**
* On search token, locally search tokens in previously loaded list to swap
* searching in names and symbols
@ -65,8 +56,8 @@ interface SwapInteractor {
@Throws(IllegalStateException::class)
suspend fun findBestQuote(
networkId: String,
fromToken: CryptoCurrency,
toToken: CryptoCurrency,
fromToken: CryptoCurrencyStatus,
toToken: CryptoCurrencyStatus,
providers: List<SwapProvider>,
amountToSwap: String,
selectedFee: FeeType = FeeType.NORMAL,

View file

@ -24,13 +24,13 @@ import com.tangem.lib.crypto.UserWalletManager
import com.tangem.lib.crypto.models.*
import com.tangem.lib.crypto.models.transactions.SendTxResult
import com.tangem.utils.toFiatString
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import timber.log.Timber
import java.math.BigDecimal
import java.math.RoundingMode
import javax.inject.Inject
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
@Suppress("LargeClass", "LongParameterList")
internal class SwapInteractorImpl @Inject constructor(
@ -150,7 +150,16 @@ internal class SwapInteractorImpl @Inject constructor(
initialCurrency: LeastTokenInfo,
currenciesList: List<CryptoCurrency>,
): List<SwapPairLeast> {
return repository.getPairs(initialCurrency, currenciesList)
val pairs = repository.getPairs(initialCurrency, currenciesList)
val providers = pairs.flatMap { it.providers }.toSet()
val updatedProviders = repository.getProvidersDetails(providers).associateBy { it.providerId }
return pairs.map { pair ->
pair.copy(
providers = pair.providers.mapNotNull { currentProvider ->
updatedProviders[currentProvider.providerId]
},
)
}
}
@Deprecated("used in old swap mechanism")
@ -159,54 +168,6 @@ internal class SwapInteractorImpl @Inject constructor(
this.network = network
}
@Deprecated("used in old swap mechanism")
override suspend fun initTokensToSwap(initialCurrency: Currency): TokensDataState {
// TODO: refactor this function
val networkId = initialCurrency.networkId
val availableTokens = cache.getAvailableTokens(networkId)
val allLoadedTokens = availableTokens.ifEmpty {
val tokens = repository.getExchangeableTokens(networkId)
cache.cacheAvailableToSwapTokens(networkId, tokens)
tokens
}.filter { it.symbol != initialCurrency.symbol }
// replace tokens in wallet tokens list with loaded same
val loadedOnWalletsMap = mutableSetOf<String>()
val tokensInWallet = userWalletManager.getUserTokens(
networkId = networkId,
derivationPath = derivationPath,
isExcludeCustom = true,
)
.map { token ->
allLoadedTokens.firstOrNull { it.symbol == token.symbol }?.let {
loadedOnWalletsMap.add(it.symbol)
it
} ?: TODO()
}
.filter { it.symbol != initialCurrency.symbol && allLoadedTokens.contains(it) }
val loadedTokens = allLoadedTokens
.filter {
!loadedOnWalletsMap.contains(it.symbol)
}
val tokensBalance = userWalletManager.getCurrentWalletTokensBalance(networkId, emptyList(), derivationPath)
.mapValues { SwapAmount(it.value.value, it.value.decimals) }
val appCurrency = userWalletManager.getUserAppCurrency()
val rates = repository.getRates(appCurrency.code, tokensInWallet.map { it.id })
cache.cacheBalances(networkId, derivationPath, tokensBalance)
// cache.cacheLoadedTokens(loadedTokens.map { TokenWithBalance(it) })
// cache.cacheInWalletTokens(getTokensWithBalance(tokensInWallet, tokensBalance, rates, appCurrency))
return TokensDataState(
preselectTokens = PreselectTokens(
fromToken = initialCurrency,
toToken = selectToToken(initialCurrency, tokensInWallet, loadedTokens),
),
foundTokensState = FoundTokensState(
tokensInWallet = emptyList(), // cache.getInWalletTokens(),
loadedTokens = emptyList(), // cache.getLoadedTokens(),
),
)
}
@Deprecated("used in old swap mechanism")
override suspend fun searchTokens(networkId: String, searchQuery: String): FoundTokensStateExpress {
val searchQueryLowerCase = searchQuery.lowercase()
@ -276,26 +237,26 @@ internal class SwapInteractorImpl @Inject constructor(
@Deprecated("used in old swap mechanism")
override suspend fun findBestQuote(
networkId: String,
fromToken: CryptoCurrency,
toToken: CryptoCurrency,
fromToken: CryptoCurrencyStatus,
toToken: CryptoCurrencyStatus,
providers: List<SwapProvider>,
amountToSwap: String,
selectedFee: FeeType,
): Map<SwapProvider, SwapState> {
syncWalletBalanceForTokens(networkId, listOf(fromToken, toToken))
syncWalletBalanceForTokens(networkId, listOf(fromToken.currency, toToken.currency))
val amountDecimal = toBigDecimalOrNull(amountToSwap)
if (amountDecimal == null || amountDecimal.signum() == 0) {
return providers.associateWith { createEmptyAmountState(networkId, fromToken, toToken) }
return providers.associateWith { createEmptyAmountState(networkId, fromToken.currency, toToken.currency) }
}
val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken))
val fromTokenAddress = getTokenAddress(fromToken)
val toTokenAddress = getTokenAddress(toToken)
val isAllowedToSpend = isAllowedToSpend(networkId, fromToken, amount)
val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken.currency))
val fromTokenAddress = getTokenAddress(fromToken.currency)
val toTokenAddress = getTokenAddress(toToken.currency)
val isAllowedToSpend = isAllowedToSpend(networkId, fromToken.currency, amount)
if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) {
allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress)
transactionManager.updateWalletManager(networkId, derivationPath)
}
val isBalanceWithoutFeeEnough = isBalanceEnough(networkId, fromToken, amount, null)
val isBalanceWithoutFeeEnough = isBalanceEnough(networkId, fromToken.currency, amount, null)
return if (isAllowedToSpend && isBalanceWithoutFeeEnough) {
// TODO
providers.associateWith {
@ -313,8 +274,8 @@ internal class SwapInteractorImpl @Inject constructor(
loadQuoteData(
networkId = networkId,
amount = amount,
fromToken = fromToken,
toToken = toToken,
fromTokenStatus = fromToken,
toTokenStatus = toToken,
isAllowedToSpend = isAllowedToSpend,
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
providers = providers,
@ -521,12 +482,14 @@ internal class SwapInteractorImpl @Inject constructor(
private suspend fun loadQuoteData(
networkId: String,
amount: SwapAmount,
fromToken: CryptoCurrency,
toToken: CryptoCurrency,
fromTokenStatus: CryptoCurrencyStatus,
toTokenStatus: CryptoCurrencyStatus,
providers: List<SwapProvider>,
isAllowedToSpend: Boolean,
isBalanceWithoutFeeEnough: Boolean,
): Map<SwapProvider, SwapState> {
val fromToken = fromTokenStatus.currency
val toToken = toTokenStatus.currency
return coroutineScope {
val quoteRequests = providers.map { provider ->
async {
@ -547,11 +510,11 @@ internal class SwapInteractorImpl @Inject constructor(
getState(
quoteDataModel = it.second,
amount = amount,
fromToken = fromToken,
toToken = toToken,
fromToken = fromTokenStatus,
toToken = toTokenStatus,
networkId = networkId,
isAllowedToSpend = isAllowedToSpend,
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
)
}.associate { it.first to it.second }
}
@ -560,8 +523,8 @@ internal class SwapInteractorImpl @Inject constructor(
private suspend fun getState(
quoteDataModel: AggregatedSwapDataModel<QuoteModel>,
amount: SwapAmount,
fromToken: CryptoCurrency,
toToken: CryptoCurrency,
fromToken: CryptoCurrencyStatus,
toToken: CryptoCurrencyStatus,
networkId: String,
isAllowedToSpend: Boolean,
isBalanceWithoutFeeEnough: Boolean,
@ -570,15 +533,15 @@ internal class SwapInteractorImpl @Inject constructor(
if (quoteModel != null) {
val swapState = updateBalances(
networkId = networkId,
fromToken = fromToken,
toToken = toToken,
fromTokenStatus = fromToken,
toTokenStatus = toToken,
fromTokenAmount = amount,
toTokenAmount = quoteModel.toTokenAmount,
swapStateData = null,
)
val quotesState = updatePermissionState(
networkId = networkId,
fromToken = fromToken,
fromToken = fromToken.currency,
swapAmount = amount,
quotesLoadedState = swapState,
)
@ -612,8 +575,8 @@ internal class SwapInteractorImpl @Inject constructor(
networkId: String,
fromTokenAddress: String,
toTokenAddress: String,
fromToken: CryptoCurrency,
toToken: CryptoCurrency,
fromToken: CryptoCurrencyStatus,
toToken: CryptoCurrencyStatus,
amount: SwapAmount,
selectedFee: FeeType,
): SwapState {
@ -630,7 +593,7 @@ internal class SwapInteractorImpl @Inject constructor(
val feeData = transactionManager.getFee(
networkId = networkId,
amountToSend = amount.value,
currencyToSend = swapCurrencyConverter.convert(fromToken),
currencyToSend = swapCurrencyConverter.convert(fromToken.currency),
destinationAddress = swapData.transaction.toWalletAddress,
increaseBy = INCREASE_GAS_LIMIT_BY,
data = swapData.transaction.data,
@ -642,17 +605,17 @@ internal class SwapInteractorImpl @Inject constructor(
FeeType.PRIORITY -> txFeeState.priorityFee.feeValue
}
val isBalanceIncludeFeeEnough =
isBalanceEnough(networkId, fromToken, amount, feeByPriority)
isBalanceEnough(networkId, fromToken.currency, amount, feeByPriority)
val isFeeEnough = checkFeeIsEnough(
fee = feeByPriority,
spendAmount = amount,
networkId = networkId,
fromToken = fromToken,
fromToken = fromToken.currency,
)
val swapState = updateBalances(
networkId = networkId,
fromToken = fromToken,
toToken = toToken,
fromTokenStatus = fromToken,
toTokenStatus = toToken,
fromTokenAmount = amount,
toTokenAmount = swapData.toTokenAmount,
swapStateData = SwapStateData(
@ -677,42 +640,32 @@ internal class SwapInteractorImpl @Inject constructor(
@Suppress("LongParameterList")
private suspend fun updateBalances(
networkId: String,
fromToken: CryptoCurrency,
toToken: CryptoCurrency,
fromTokenStatus: CryptoCurrencyStatus,
toTokenStatus: CryptoCurrencyStatus,
fromTokenAmount: SwapAmount,
toTokenAmount: SwapAmount,
swapStateData: SwapStateData?,
): SwapState.QuotesLoadedState {
val fromToken = fromTokenStatus.currency
val toToken = toTokenStatus.currency
val appCurrency = userWalletManager.getUserAppCurrency()
val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId)
val rates = repository.getRates(
appCurrency.code,
listOf(fromToken.network.backendId, toToken.network.backendId, nativeToken.id),
)
val fromTokenBalance = cache.getBalanceForToken(networkId, derivationPath, fromToken.symbol)
val toTokenBalance = cache.getBalanceForToken(networkId, derivationPath, toToken.symbol)
return SwapState.QuotesLoadedState(
fromTokenInfo = TokenSwapInfo(
tokenAmount = fromTokenAmount,
coinId = fromToken.id.value,
tokenWalletBalance = fromTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") }
?: ZERO_BALANCE,
tokenFiatBalance = fromTokenAmount.value.toFiatString(
rateValue = rates[fromToken.network.backendId]?.toBigDecimal() ?: BigDecimal.ZERO,
fiatCurrencyName = appCurrency.symbol,
formatWithSpaces = true,
),
cryptoCurrencyStatus = fromTokenStatus,
amountFiat = rates[fromToken.network.backendId]?.toBigDecimal()?.multiply(fromTokenAmount.value)
?: BigDecimal.ZERO,
),
toTokenInfo = TokenSwapInfo(
tokenAmount = toTokenAmount,
coinId = toToken.id.value,
tokenWalletBalance = toTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") }
?: ZERO_BALANCE,
tokenFiatBalance = toTokenAmount.value.toFiatString(
rateValue = rates[toToken.network.backendId]?.toBigDecimal() ?: BigDecimal.ZERO,
fiatCurrencyName = appCurrency.symbol,
formatWithSpaces = true,
),
cryptoCurrencyStatus = toTokenStatus,
amountFiat = rates[toToken.network.backendId]?.toBigDecimal()?.multiply(toTokenAmount.value)
?: BigDecimal.ZERO,
),
priceImpact = calculatePriceImpact(
fromTokenAmount = fromTokenAmount.value,
@ -933,7 +886,6 @@ internal class SwapInteractorImpl @Inject constructor(
companion object {
private const val DEFAULT_SLIPPAGE = 2
private const val ZERO_BALANCE = "0"
@Suppress("UnusedPrivateMember")
private const val INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT = 1.0 // if need to increase fee when check isEnough

View file

@ -10,6 +10,8 @@ interface SwapRepository {
suspend fun getPairs(initialCurrency: LeastTokenInfo, currencyList: List<CryptoCurrency>): List<SwapPairLeast>
suspend fun getProvidersDetails(providers: Set<SwapProvider>): List<SwapProvider>
suspend fun getRates(currencyId: String, tokenIds: List<String>): Map<String, Double>
suspend fun getExchangeableTokens(networkId: String): List<Currency>
@ -21,7 +23,7 @@ interface SwapRepository {
toContractAddress: String,
toNetwork: String,
fromAmount: String,
providerId: Int,
providerId: String,
rateType: RateType,
): AggregatedSwapDataModel<QuoteModel>
@ -73,7 +75,7 @@ interface SwapRepository {
toContractAddress: String,
toNetwork: String,
fromAmount: String,
providerId: Int,
providerId: String,
rateType: RateType,
): ExchangeQuote
}

View file

@ -1,7 +1,6 @@
package com.tangem.feature.swap.domain.di
import com.tangem.domain.tokens.GetCardTokensListUseCase
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository

View file

@ -27,10 +27,18 @@ data class CryptoCurrencySwapInfo(
* @property rateTypes supported rate types
*/
data class SwapProvider(
val providerId: Int,
val rateTypes: List<RateType>,
val providerId: String,
val rateTypes: List<RateType> = emptyList(),
val name: String? = null,
val type: ExchangeProviderType? = null,
val imageLarge: String? = null,
)
enum class ExchangeProviderType {
DEX,
CEX,
}
/**
* Rate type.
*

View file

@ -1,5 +1,6 @@
package com.tangem.feature.swap.domain.models.ui
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.PreparedSwapConfigState
@ -51,9 +52,8 @@ sealed class PermissionDataState {
data class TokenSwapInfo(
val tokenAmount: SwapAmount,
val coinId: String,
val tokenWalletBalance: String,
val tokenFiatBalance: String,
val amountFiat: BigDecimal,
val cryptoCurrencyStatus: CryptoCurrencyStatus,
)
data class RequestApproveStateData(

View file

@ -5,6 +5,7 @@ import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.components.states.Item
import com.tangem.core.ui.components.states.SelectableItemsState
import com.tangem.feature.swap.domain.models.ui.TxFee
import com.tangem.feature.swap.models.states.ProviderState
data class SwapStateHolder(
val sendCardData: SwapCardState,
@ -16,6 +17,7 @@ data class SwapStateHolder(
val warnings: List<SwapWarning> = emptyList(),
val alert: SwapWarning.GenericWarning? = null,
val updateInProgress: Boolean = false,
val providerState: ProviderState,
val permissionState: SwapPermissionState = SwapPermissionState.Empty,
val successState: SwapSuccessStateHolder? = null,

View file

@ -5,6 +5,11 @@ sealed class ProviderState {
abstract val onProviderClick: ((String) -> Unit)?
abstract val id: String
data class Empty(
override val id: String = "",
override val onProviderClick: ((String) -> Unit)? = null,
) : ProviderState()
data class Loading(
override val id: String = "",
override val onProviderClick: ((String) -> Unit)? = null,

View file

@ -36,11 +36,13 @@ private val GrayscaleColorFilter: ColorFilter
@Composable
fun ProviderItemBlock(state: ProviderState) {
BaseContainer(state) {
ProviderItem(
state = state,
modifier = Modifier.align(Alignment.CenterStart),
)
if (state !is ProviderState.Empty) {
BaseContainer(state) {
ProviderItem(
state = state,
modifier = Modifier.align(Alignment.CenterStart),
)
}
}
}
@ -65,6 +67,9 @@ fun ProviderItem(state: ProviderState, modifier: Modifier = Modifier, isSelected
modifier = modifier,
)
}
is ProviderState.Empty -> {
// do nothing
}
}
}

View file

@ -17,12 +17,15 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.swap.converters.TokensDataConverter
import com.tangem.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.domain.NetworkInfo
import com.tangem.feature.swap.domain.models.domain.SwapProvider
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
import com.tangem.feature.swap.domain.models.ui.*
import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.models.states.ProviderState
import com.tangem.feature.swap.presentation.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal
/**
* State builder creates a specific states for SwapScreen
@ -51,7 +54,7 @@ internal class StateBuilder(
amountTextFieldValue = null,
tokenIconUrl = initialCurrency.iconUrl,
tokenCurrency = initialCurrency.symbol,
coinId = null,
coinId = initialCurrency.network.backendId,
canSelectAnotherToken = false,
isNotNativeToken = initialCurrency is CryptoCurrency.Token,
balance = "",
@ -79,6 +82,7 @@ internal class StateBuilder(
updateInProgress = true,
onShowPermissionBottomSheet = actions.openPermissionBottomSheet,
onCancelPermissionBottomSheet = actions.hidePermissionBottomSheet,
providerState = ProviderState.Loading(),
)
}
@ -146,7 +150,7 @@ internal class StateBuilder(
amountEquivalent = null,
tokenIconUrl = fromToken.iconUrl,
tokenCurrency = fromToken.symbol,
coinId = fromToken.id.value,
coinId = fromToken.network.backendId,
isNotNativeToken = fromToken is CryptoCurrency.Token,
canSelectAnotherToken = canSelectSendToken,
balance = if (!canSelectSendToken) uiStateHolder.sendCardData.balance else "",
@ -158,7 +162,7 @@ internal class StateBuilder(
amountEquivalent = null,
tokenIconUrl = toToken.iconUrl,
tokenCurrency = toToken.symbol,
coinId = toToken.id.value,
coinId = toToken.network.backendId,
isNotNativeToken = toToken is CryptoCurrency.Token,
canSelectAnotherToken = canSelectReceiveToken,
balance = if (!canSelectReceiveToken) uiStateHolder.receiveCardData.balance else "",
@ -185,6 +189,7 @@ internal class StateBuilder(
uiStateHolder: SwapStateHolder,
quoteModel: SwapState.QuotesLoadedState,
fromToken: CryptoCurrency,
swapProvider: SwapProvider,
onFeeSetup: (TxFee) -> Unit,
): SwapStateHolder {
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
@ -213,29 +218,31 @@ internal class StateBuilder(
)
}
val feeState = createFeeState(quoteModel, uiStateHolder, onFeeSetup)
val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus
val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus
return uiStateHolder.copy(
sendCardData = SwapCardState.SwapCardData(
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue,
amountEquivalent = quoteModel.fromTokenInfo.tokenFiatBalance,
amountEquivalent = fromCurrencyStatus.getFormattedAmount(quoteModel.fromTokenInfo.amountFiat),
tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl,
coinId = quoteModel.fromTokenInfo.coinId,
coinId = fromCurrencyStatus.currency.network.backendId,
isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken,
tokenCurrency = uiStateHolder.sendCardData.tokenCurrency,
canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken,
balance = quoteModel.fromTokenInfo.tokenWalletBalance,
balance = fromCurrencyStatus.getFormattedAmount(),
isBalanceHidden = isBalanceHiddenProvider(),
),
receiveCardData = SwapCardState.SwapCardData(
type = TransactionCardType.ReceiveCard(),
amountTextFieldValue = TextFieldValue(quoteModel.toTokenInfo.tokenAmount.formatToUIRepresentation()),
amountEquivalent = quoteModel.toTokenInfo.tokenFiatBalance,
amountEquivalent = toCurrencyStatus.getFormattedAmount(quoteModel.toTokenInfo.amountFiat),
tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl,
coinId = quoteModel.toTokenInfo.coinId,
coinId = toCurrencyStatus.currency.network.backendId,
isNotNativeToken = uiStateHolder.receiveCardData.isNotNativeToken,
tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency,
canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken,
balance = quoteModel.toTokenInfo.tokenWalletBalance,
balance = toCurrencyStatus.getFormattedAmount(),
isBalanceHidden = isBalanceHiddenProvider(),
),
networkCurrency = quoteModel.networkCurrency,
@ -256,6 +263,10 @@ internal class StateBuilder(
onClick = actions.onSwapClick,
),
updateInProgress = false,
providerState = swapProvider.convertToContentClickableProviderState(
fromTokenInfo = quoteModel.fromTokenInfo,
toTokenInfo = quoteModel.toTokenInfo,
),
)
}
@ -298,6 +309,7 @@ internal class StateBuilder(
onClick = { },
),
updateInProgress = false,
providerState = ProviderState.Empty(),
)
}
@ -726,12 +738,37 @@ internal class StateBuilder(
return "$firstAddressPart...$secondAddressPart"
}
private fun SwapProvider.convertToContentClickableProviderState(
fromTokenInfo: TokenSwapInfo,
toTokenInfo: TokenSwapInfo,
): ProviderState {
val rate = fromTokenInfo.tokenAmount.value / toTokenInfo.tokenAmount.value
val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol
val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol
val rateString = "1 $fromCurrencySymbol$rate $toCurrencySymbol"
return ProviderState.Content(
id = this.providerId,
name = this.name ?: "",
iconUrl = this.imageLarge ?: "",
type = this.type.toString(),
rate = rateString,
additionalBadge = ProviderState.AdditionalBadge.BestTrade,
selectionType = ProviderState.SelectionType.CLICK,
percentLowerThenBest = null,
onProviderClick = {},
)
}
private fun CryptoCurrencyStatus.getFormattedAmount(): String {
val amount = value.amount ?: return UNKNOWN_AMOUNT_SIGN
return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals)
}
private fun CryptoCurrencyStatus.getFormattedAmount(amount: BigDecimal): String {
return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals)
}
@Suppress("UnusedPrivateMember")
private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String {
val fiatAmount = value.fiatAmount ?: return UNKNOWN_AMOUNT_SIGN

View file

@ -32,6 +32,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.domain.models.ui.FeeType
import com.tangem.feature.swap.domain.models.ui.TxFee
import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.models.states.ProviderState
import com.tangem.feature.swap.presentation.R
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal
@ -77,6 +78,8 @@ internal fun SwapScreenContent(state: SwapStateHolder, onPermissionWarningClick:
) {
MainInfo(state)
ProviderItemBlock(state = state.providerState)
FeeItem(feeState = state.fee, currency = state.networkCurrency)
if (state.warnings.isNotEmpty()) SwapWarnings(warnings = state.warnings)
@ -476,6 +479,7 @@ private val state = SwapStateHolder(
onChangeCardsClicked = {},
permissionState = SwapPermissionState.InProgress,
blockchainId = "POLYGON",
providerState = ProviderState.Loading(),
)
@Preview

View file

@ -2,10 +2,8 @@ package com.tangem.feature.swap.viewmodels
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData
import com.tangem.feature.swap.domain.models.ui.SwapStateData
import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress
import com.tangem.feature.swap.domain.models.ui.TxFee
import com.tangem.feature.swap.domain.models.domain.SwapProvider
import com.tangem.feature.swap.domain.models.ui.*
data class SwapProcessDataState(
// Initial network id
@ -22,4 +20,6 @@ data class SwapProcessDataState(
val swapDataModel: SwapStateData? = null,
val selectedFee: TxFee? = null,
val tokensDataState: TokensDataStateExpress? = null,
val selectedProvider: SwapProvider? = null,
val lastLoadedSwapStates: Map<SwapProvider, SwapState> = emptyMap(),
)

View file

@ -18,7 +18,6 @@ import com.tangem.feature.swap.analytics.SwapEvents
import com.tangem.feature.swap.domain.BlockchainInteractor
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.feature.swap.domain.models.domain.PermissionOptions
import com.tangem.feature.swap.domain.models.domain.RateType
import com.tangem.feature.swap.domain.models.domain.SwapProvider
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
import com.tangem.feature.swap.domain.models.ui.*
@ -74,7 +73,7 @@ internal class SwapViewModel @Inject constructor(
private val inputNumberFormatter =
InputNumberFormatter(NumberFormat.getInstance(Locale.getDefault()) as DecimalFormat)
private val amountDebouncer = Debouncer()
private val singleTaskScheduler = SingleTaskScheduler<SwapState>()
private val singleTaskScheduler = SingleTaskScheduler<Map<SwapProvider, SwapState>>()
private var dataState by mutableStateOf(SwapProcessDataState(networkId = initialCryptoCurrency.network.backendId))
@ -174,10 +173,10 @@ internal class SwapViewModel @Inject constructor(
)
} else {
startLoadingQuotes(
fromToken = fromCurrencyStatus.currency,
toToken = selectedCurrency.currency,
fromToken = fromCurrencyStatus,
toToken = selectedCurrency,
amount = lastAmount.value,
toProvidersList = findSwapProviders(fromCurrencyStatus, selectedCurrency)
toProvidersList = findSwapProviders(fromCurrencyStatus, selectedCurrency),
)
}
}
@ -196,13 +195,18 @@ internal class SwapViewModel @Inject constructor(
}
private fun startLoadingQuotes(
fromToken: CryptoCurrency,
toToken: CryptoCurrency,
fromToken: CryptoCurrencyStatus,
toToken: CryptoCurrencyStatus,
amount: String,
toProvidersList: List<SwapProvider>,
) {
singleTaskScheduler.cancelTask()
uiState = stateBuilder.createQuotesLoadingState(uiState, fromToken, toToken, initialCryptoCurrency.id.value)
uiState = stateBuilder.createQuotesLoadingState(
uiState,
fromToken.currency,
toToken.currency,
initialCryptoCurrency.id.value,
)
singleTaskScheduler.scheduleTask(
viewModelScope,
loadQuotesTask(
@ -220,20 +224,20 @@ internal class SwapViewModel @Inject constructor(
val amount = dataState.amount
if (fromCurrency != null && toCurrency != null && amount != null) {
startLoadingQuotes(
fromToken = fromCurrency.currency,
toToken = toCurrency.currency,
fromToken = fromCurrency,
toToken = toCurrency,
amount = amount,
toProvidersList = findSwapProviders(fromCurrency, toCurrency)
toProvidersList = findSwapProviders(fromCurrency, toCurrency),
)
}
}
private fun loadQuotesTask(
fromToken: CryptoCurrency,
toToken: CryptoCurrency,
fromToken: CryptoCurrencyStatus,
toToken: CryptoCurrencyStatus,
amount: String,
toProvidersList: List<SwapProvider>,
): PeriodicTask<SwapState> {
): PeriodicTask<Map<SwapProvider, SwapState>> {
return PeriodicTask(
UPDATE_DELAY,
task = {
@ -251,17 +255,19 @@ internal class SwapViewModel @Inject constructor(
providers = toProvidersList,
amountToSwap = amount,
selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL,
).entries.first().value// TODO
)
}
},
onSuccess = { swapState ->
when (swapState) {
onSuccess = { providersState ->
val (provider, state) = updateLoadedQuotes(providersState)
when (state) {
is SwapState.QuotesLoadedState -> {
fillDataState(swapState.permissionState, swapState.swapDataModel)
fillDataState(state.permissionState, state.swapDataModel)
uiState = stateBuilder.createQuotesLoadedState(
uiStateHolder = uiState,
quoteModel = swapState,
fromToken = fromToken,
quoteModel = state,
fromToken = fromToken.currency,
swapProvider = provider,
) { updatedFee ->
dataState = dataState.copy(
selectedFee = updatedFee,
@ -271,12 +277,12 @@ internal class SwapViewModel @Inject constructor(
is SwapState.EmptyAmountState -> {
uiState = stateBuilder.createQuotesEmptyAmountState(
uiStateHolder = uiState,
emptyAmountState = swapState,
emptyAmountState = state,
)
}
is SwapState.SwapError -> {
Timber.e("SwapError when loading quotes ${swapState.error}")
uiState = stateBuilder.mapError(uiState, swapState.error) { startLoadingQuotesFromLastState() }
Timber.e("SwapError when loading quotes ${state.error}")
uiState = stateBuilder.mapError(uiState, state.error) { startLoadingQuotesFromLastState() }
}
}
},
@ -287,6 +293,15 @@ internal class SwapViewModel @Inject constructor(
)
}
private fun updateLoadedQuotes(state: Map<SwapProvider, SwapState>): Pair<SwapProvider, SwapState> {
val selectedSwapProvider = dataState.selectedProvider ?: state.keys.first()
dataState = dataState.copy(
selectedProvider = selectedSwapProvider,
lastLoadedSwapStates = state,
)
return state.entries.first { it.key == selectedSwapProvider }.toPair()
}
private fun fillDataState(permissionState: PermissionDataState, swapDataModel: SwapStateData?) {
dataState = if (permissionState is PermissionDataState.PermissionReadyForRequest) {
dataState.copy(
@ -435,10 +450,10 @@ internal class SwapViewModel @Inject constructor(
toCryptoCurrency = toToken,
)
startLoadingQuotes(
fromToken = fromToken.currency,
toToken = toToken.currency,
fromToken = fromToken,
toToken = toToken,
amount = lastAmount.value,
toProvidersList = findSwapProviders(fromToken, toToken)
toProvidersList = findSwapProviders(fromToken, toToken),
)
swapRouter.openScreen(SwapNavScreen.Main)
}
@ -460,10 +475,10 @@ internal class SwapViewModel @Inject constructor(
inputNumberFormatter.formatWithThousands(lastAmount.value, decimals),
)
startLoadingQuotes(
fromToken = newFromToken.currency,
toToken = newToToken.currency,
fromToken = newFromToken,
toToken = newToToken,
amount = lastAmount.value,
toProvidersList = findSwapProviders(newFromToken, newToToken)
toProvidersList = findSwapProviders(newFromToken, newToToken),
)
}
}
@ -479,10 +494,10 @@ internal class SwapViewModel @Inject constructor(
stateBuilder.updateSwapAmount(uiState, inputNumberFormatter.formatWithThousands(cutValue, decimals))
amountDebouncer.debounce(viewModelScope, DEBOUNCE_AMOUNT_DELAY) {
startLoadingQuotes(
fromToken = fromToken.currency,
toToken = toToken.currency,
fromToken = fromToken,
toToken = toToken,
amount = lastAmount.value,
toProvidersList = findSwapProviders(fromToken, toToken)
toProvidersList = findSwapProviders(fromToken, toToken),
)
}
}