Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-24 15:17:25 +05:00
parent 94b459d7c2
commit 13fd91fc8c
11 changed files with 93 additions and 101 deletions

View file

@ -216,7 +216,9 @@ sealed class CryptoCurrency {
private const val DERIVATION_PATH_DELIMITER = '\u2192' // →
private const val ID_PARTS_COUNT = 3
const val MAIN_NETWORK_TYPE_NAME = "MAIN"
const val MAIN_NETWORK_L2_TYPE_NAME = "MAIN L2"
/**
* Creates an [ID] from a string [value].

View file

@ -24,10 +24,10 @@ data class SwapCurrencies(
/**
* Return swap group depending on [swapDirection]
*/
fun SwapCurrencies.getGroupWithReverse(swapDirection: SwapDirection): SwapCurrenciesGroup {
fun SwapCurrencies.getGroupWithDirection(swapDirection: SwapDirection): SwapCurrenciesGroup {
return when (swapDirection) {
SwapDirection.Reverse -> fromGroup
SwapDirection.Direct -> toGroup
SwapDirection.Reverse -> toGroup
SwapDirection.Direct -> fromGroup
}
}

View file

@ -4,11 +4,12 @@ import arrow.core.Either
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.express.models.ExpressRateType
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.swap.SwapErrorResolver
import com.tangem.domain.swap.SwapRepositoryV2
import com.tangem.domain.swap.models.SwapDataModel
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
@Suppress("LongParameterList")
class GetSwapDataUseCase(
@ -20,8 +21,8 @@ class GetSwapDataUseCase(
userWallet: UserWallet,
fromCryptoCurrencyStatus: CryptoCurrencyStatus,
fromAmount: String,
toCryptoCurrencyStatus: CryptoCurrencyStatus,
toAddress: String?,
toCryptoCurrency: CryptoCurrency,
toAddress: String,
expressProvider: ExpressProvider,
rateType: ExpressRateType,
): Either<ExpressError, SwapDataModel> = Either.catch {
@ -29,7 +30,7 @@ class GetSwapDataUseCase(
userWallet = userWallet,
fromCryptoCurrencyStatus = fromCryptoCurrencyStatus,
fromAmount = fromAmount,
toCryptoCurrencyStatus = toCryptoCurrencyStatus,
toCryptoCurrency = toCryptoCurrency,
toAddress = toAddress,
expressProvider = expressProvider,
rateType = rateType,

View file

@ -1,13 +1,13 @@
package com.tangem.domain.swap.usecase
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.swap.SwapTransactionRepository
import com.tangem.domain.swap.models.SwapCurrencies
import com.tangem.domain.swap.models.SwapCurrenciesGroup
import com.tangem.domain.swap.models.SwapDirection
import com.tangem.domain.swap.models.getGroupWithReverse
import com.tangem.domain.swap.models.getGroupWithDirection
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.utils.extensions.orZero
/**
@ -31,7 +31,7 @@ class SelectInitialPairUseCase(
swapCurrencies: SwapCurrencies,
swapDirection: SwapDirection,
): CryptoCurrencyStatus? {
val swapCurrenciesGroup = swapCurrencies.getGroupWithReverse(swapDirection)
val swapCurrenciesGroup = swapCurrencies.getGroupWithDirection(swapDirection)
return tryToGetAlreadySelectedCurrency(secondaryCryptoCurrency, swapCurrenciesGroup)
?: tryGetFromCache(userWallet, primaryCryptoCurrency, swapCurrenciesGroup)
?: tryGetWithMaxAmount(swapCurrenciesGroup)

View file

@ -62,11 +62,11 @@ internal class DefaultChooseManagedTokensComponent @AssistedInject constructor(
initialCurrency = config.initialCurrency,
token = config.token,
onDismiss = model.bottomSheetNavigation::dismiss,
onResult = {
onResult = { swapCurrencies, cryptoCurrency ->
componentScope.launch {
swapChooseTokenNetworkTrigger.trigger(it)
swapChooseTokenNetworkTrigger.trigger(swapCurrencies, cryptoCurrency)
router.pop()
}
router.pop()
},
),
)

View file

@ -5,6 +5,7 @@ import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.swap.models.SwapCurrencies
interface SwapChooseTokenNetworkComponent : ComposableBottomSheetComponent {
@ -13,7 +14,7 @@ interface SwapChooseTokenNetworkComponent : ComposableBottomSheetComponent {
val initialCurrency: CryptoCurrency,
val token: ManagedCryptoCurrency.Token,
val onDismiss: () -> Unit,
val onResult: (CryptoCurrency) -> Unit,
val onResult: (SwapCurrencies, CryptoCurrency) -> Unit,
)
interface Factory : ComponentFactory<Params, SwapChooseTokenNetworkComponent>

View file

@ -1,6 +1,7 @@
package com.tangem.features.swap.v2.api.choosetoken
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.swap.models.SwapCurrencies
import kotlinx.coroutines.flow.SharedFlow
/**
@ -8,12 +9,12 @@ import kotlinx.coroutines.flow.SharedFlow
*/
interface SwapChooseTokenNetworkTrigger {
suspend fun trigger(cryptoCurrency: CryptoCurrency)
suspend fun trigger(swapCurrencies: SwapCurrencies, cryptoCurrency: CryptoCurrency)
}
/**
* Listens to swap token selection
*/
interface SwapChooseTokenNetworkListener {
val swapChooseTokenNetworkResultFlow: SharedFlow<CryptoCurrency>
val swapChooseTokenNetworkResultFlow: SharedFlow<Pair<SwapCurrencies, CryptoCurrency>>
}

View file

@ -17,14 +17,14 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.swap.models.SwapCurrencies
import com.tangem.domain.swap.models.SwapDirection
import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection
import com.tangem.domain.swap.models.SwapQuoteModel
import com.tangem.domain.swap.usecase.GetSwapPairsUseCase
import com.tangem.domain.swap.models.getGroupWithDirection
import com.tangem.domain.swap.usecase.GetSwapQuoteUseCase
import com.tangem.domain.swap.usecase.SelectInitialPairUseCase
import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase
import com.tangem.domain.tokens.GetMultiCryptoCurrencyStatusUseCase
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.transaction.usecase.GetAllowanceUseCase
import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger
@ -60,8 +60,6 @@ import com.tangem.utils.transformer.update as transformerUpdate
internal class SwapAmountModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val getSwapPairsUseCase: GetSwapPairsUseCase,
private val getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase,
private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase,
private val selectInitialPairUseCase: SelectInitialPairUseCase,
private val getSwapQuoteUseCase: GetSwapQuoteUseCase,
@ -304,10 +302,10 @@ internal class SwapAmountModel @Inject constructor(
private fun observeChooseSelectToken() {
swapChooseTokenNetworkListener.swapChooseTokenNetworkResultFlow
.onEach { currency ->
.onEach { (swapCurrencies, currency) ->
uiState.update { amountUM ->
if (amountUM is SwapAmountUM.Content) {
initPairs(currency)
initPairs(swapCurrencies, currency)
amountUM.copy(
isPrimaryButtonEnabled = false,
secondaryAmount = SwapAmountFieldUM.Loading(
@ -322,65 +320,42 @@ internal class SwapAmountModel @Inject constructor(
.launchIn(modelScope)
}
private fun initPairs(secondaryCryptoCurrency: CryptoCurrency?) {
private fun initPairs(swapCurrencies: SwapCurrencies, secondaryCryptoCurrency: CryptoCurrency?) {
modelScope.launch {
val cryptoCurrencyStatusList = getMultiCryptoCurrencyStatusUseCase
.invokeMultiWalletSync(userWallet.walletId)
.getOrElse { emptyList() }
val cryptoCurrencyStatusListExceptPrimary = cryptoCurrencyStatusList.filter {
val statusFilter = it.value is CryptoCurrencyStatus.Loaded || it.value is CryptoCurrencyStatus.NoAccount
val notCustomTokenFilter = !it.currency.isCustom
statusFilter && notCustomTokenFilter
}
getSwapPairsUseCase(
val secondaryStatus = selectInitialPairUseCase(
primaryCryptoCurrency = primaryCryptoCurrency,
secondaryCryptoCurrency = secondaryCryptoCurrency,
userWallet = userWallet,
initialCurrency = primaryCryptoCurrency,
cryptoCurrencyStatusList = cryptoCurrencyStatusListExceptPrimary,
filterProviderTypes = params.filterProviderTypes,
).fold(
ifRight = { swapCurrencies ->
val secondaryStatus = selectInitialPairUseCase(
primaryCryptoCurrency = primaryCryptoCurrency,
secondaryCryptoCurrency = secondaryCryptoCurrency,
swapCurrencies = swapCurrencies,
swapDirection = params.swapDirection,
)
val primaryStatus = (uiState.value as? SwapAmountUM.Content)?.primaryCryptoCurrencyStatus
if (secondaryStatus != null && primaryStatus != null) {
initCurrencies(primaryStatus, secondaryStatus)
uiState.transformerUpdate(
SwapAmountSecondaryReadyStateTransformer(
userWallet = userWallet,
swapCurrencies = swapCurrencies,
swapDirection = params.swapDirection,
)
val primaryStatus = (uiState.value as? SwapAmountUM.Content)?.primaryCryptoCurrencyStatus
if (secondaryStatus != null && primaryStatus != null) {
initCurrencies(primaryStatus, secondaryStatus)
uiState.transformerUpdate(
SwapAmountSecondaryReadyStateTransformer(
userWallet = userWallet,
swapCurrencies = swapCurrencies,
primaryCryptoCurrencyStatus = primaryStatus,
secondaryCryptoCurrencyStatus = secondaryStatus,
appCurrency = appCurrency,
swapDirection = swapDirection,
clickIntents = this@SwapAmountModel,
isBalanceHidden = params.isBalanceHidingFlow.value,
),
)
loadQuotes()
} else {
Timber.e(
"""
Invalid cryptocurrencies status:
| Primary -> $primaryStatus
| Secondary -> $secondaryStatus
""".trimIndent(),
)
showErrorAlert(errorMessage = null)
}
},
ifLeft = { error ->
Timber.e(error)
showErrorAlert(errorMessage = error.message)
},
)
primaryCryptoCurrencyStatus = primaryStatus,
secondaryCryptoCurrencyStatus = secondaryStatus,
appCurrency = appCurrency,
swapDirection = swapDirection,
clickIntents = this@SwapAmountModel,
isBalanceHidden = params.isBalanceHidingFlow.value,
),
)
loadQuotes()
} else {
Timber.e(
"""
Invalid cryptocurrencies status:
| Primary -> $primaryStatus
| Secondary -> $secondaryStatus
""".trimIndent(),
)
showErrorAlert(errorMessage = null)
}
}
}
@ -428,15 +403,12 @@ internal class SwapAmountModel @Inject constructor(
val fromAmountValue = fromAmount?.amountTextField?.cryptoAmount?.value ?: return
val swapGroups = when (state.swapDirection) {
SwapDirection.Direct -> state.swapCurrencies.toGroup.available
SwapDirection.Reverse -> state.swapCurrencies.fromGroup.available
}
val swapGroups = state.swapCurrencies.getGroupWithDirection(state.swapDirection)
uiState.transformerUpdate(SwapQuoteLoadingStateTransformer)
modelScope.launch {
val quotes = swapGroups.filter {
val quotes = swapGroups.available.filter {
it.currencyStatus.currency.id == toCryptoCurrency.id
}.flatMap {
it.providers

View file

@ -1,6 +1,7 @@
package com.tangem.features.swap.v2.impl.choosetoken.fromSupported
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.swap.models.SwapCurrencies
import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkListener
import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkTrigger
import kotlinx.coroutines.flow.MutableSharedFlow
@ -13,10 +14,10 @@ internal class DefaultSwapChooseTokenNetworkTrigger @Inject constructor() :
SwapChooseTokenNetworkTrigger,
SwapChooseTokenNetworkListener {
override val swapChooseTokenNetworkResultFlow: SharedFlow<CryptoCurrency>
field = MutableSharedFlow<CryptoCurrency>()
override val swapChooseTokenNetworkResultFlow: SharedFlow<Pair<SwapCurrencies, CryptoCurrency>>
field = MutableSharedFlow<Pair<SwapCurrencies, CryptoCurrency>>()
override suspend fun trigger(cryptoCurrency: CryptoCurrency) {
swapChooseTokenNetworkResultFlow.emit(cryptoCurrency)
override suspend fun trigger(swapCurrencies: SwapCurrencies, cryptoCurrency: CryptoCurrency) {
swapChooseTokenNetworkResultFlow.emit(swapCurrencies to cryptoCurrency)
}
}

View file

@ -4,36 +4,48 @@ import com.tangem.core.ui.extensions.iconResId
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.express.models.ExpressRateType
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency.ID.Companion.MAIN_NETWORK_L2_TYPE_NAME
import com.tangem.domain.models.currency.CryptoCurrency.ID.Companion.MAIN_NETWORK_TYPE_NAME
import com.tangem.domain.models.network.Network
import com.tangem.domain.swap.models.SwapCurrencies
import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseNetworkUM
import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseTokenNetworkContentUM
import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseTokenNetworkUM
import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model.SwapChooseTokenFactory.getErrorMessage
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.toPersistentList
internal class SwapChooseContentStateTransformer(
private val pairs: SwapCurrencies,
private val tokenName: String,
private val onNetworkClick: (CryptoCurrency) -> Unit,
private val onNetworkClick: (SwapCurrencies, CryptoCurrency) -> Unit,
private val onDismiss: () -> Unit,
) : Transformer<SwapChooseTokenNetworkUM> {
override fun transform(prevState: SwapChooseTokenNetworkUM): SwapChooseTokenNetworkUM {
val swapNetworks = pairs.fromGroup.available.map {
val network = it.currencyStatus.currency.network
val isMain = it.currencyStatus.currency is CryptoCurrency.Coin
val swapNetworks = pairs.fromGroup.available.map { availableCurrency ->
val cryptoCurrency = availableCurrency.currencyStatus.currency
val network = cryptoCurrency.network
val isMain = cryptoCurrency is CryptoCurrency.Coin
val subtitle = when {
BlockchainUtils.isL2Network(networkId = network.backendId) -> MAIN_NETWORK_L2_TYPE_NAME
isMain -> MAIN_NETWORK_TYPE_NAME
network.standardType !is Network.StandardType.Unspecified -> network.standardType.name
else -> ""
}
SwapChooseNetworkUM(
title = stringReference(network.name),
subtitle = if (isMain) {
stringReference(MAIN_NETWORK_TYPE_NAME)
} else {
stringReference(network.standardType.name)
},
subtitle = stringReference(subtitle),
iconResId = network.iconResId,
isMainNetwork = isMain,
hasFixedRate = it.providers.any { it.rateTypes.any { it == ExpressRateType.Fixed } },
onNetworkClick = { onNetworkClick(it.currencyStatus.currency) },
hasFixedRate = availableCurrency.providers.any { provider ->
provider.rateTypes.any { rateType ->
rateType == ExpressRateType.Fixed
}
},
onNetworkClick = { onNetworkClick(pairs, availableCurrency.currencyStatus.currency) },
)
}.toPersistentList()

View file

@ -5,6 +5,7 @@ import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.express.models.ExpressProviderType
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.swap.models.SwapDataModel
import com.tangem.domain.swap.models.SwapDataTransactionModel
import com.tangem.domain.swap.usecase.GetSwapDataUseCase
@ -14,7 +15,6 @@ import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.domain.transaction.usecase.CreateTransferTransactionUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.utils.convertToSdkAmount
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils
import com.tangem.features.swap.v2.impl.common.ConfirmData
import dagger.assisted.Assisted
@ -22,6 +22,7 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import timber.log.Timber
import java.math.BigDecimal
import java.math.RoundingMode
@Suppress("LongParameterList")
internal class SwapTransactionSender @AssistedInject constructor(
@ -72,6 +73,7 @@ internal class SwapTransactionSender @AssistedInject constructor(
val rateType = confirmData.rateType ?: return
val amountValue = confirmData.enteredAmount ?: return
val feeValue = confirmData.fee?.amount?.value ?: return
val destination = confirmData.enteredDestination ?: return
val fromAmount = FeeCalculationUtils.checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable = isAmountSubtractAvailable,
@ -85,8 +87,8 @@ internal class SwapTransactionSender @AssistedInject constructor(
userWallet = userWallet,
fromCryptoCurrencyStatus = fromStatus,
fromAmount = fromAmount.toStringWithRightOffset(fromStatus.currency.decimals),
toCryptoCurrencyStatus = toStatus,
toAddress = confirmData.enteredDestination,
toCryptoCurrency = toStatus.currency,
toAddress = destination,
expressProvider = provider,
rateType = rateType,
).getOrElse { onExpressError(it); return }
@ -163,7 +165,7 @@ internal class SwapTransactionSender @AssistedInject constructor(
}
private fun BigDecimal.toStringWithRightOffset(decimals: Int): String {
return movePointRight(decimals).toPlainString()
return setScale(decimals, RoundingMode.HALF_DOWN).movePointRight(decimals).toPlainString()
}
@AssistedFactory