Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-24 15:16:52 +05:00
parent ae79b95521
commit 94b459d7c2
13 changed files with 131 additions and 85 deletions

View file

@ -125,7 +125,7 @@ class AmountStateConverterV2(
resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat))
},
tokenName = stringReference(cryptoCurrencyStatus.currency.name),
tokenIconState = iconStateConverter.convert(cryptoCurrencyStatus),
tokenIconState = iconStateConverter.convert(cryptoCurrencyStatus.currency),
amountTextField = amountFieldConverter.convert(value.value),
isPrimaryButtonEnabled = false,
appCurrency = appCurrency,

View file

@ -31,6 +31,10 @@ dependencies {
implementation(projects.domain.tokens)
implementation(projects.domain.legacy)
implementation(projects.domain.models)
implementation(projects.domain.quotes)
implementation(projects.domain.networks)
implementation(projects.domain.staking.models)
implementation(projects.domain.staking)
/** Tangem SDK */
implementation(tangemDeps.blockchain) {

View file

@ -1,5 +1,6 @@
package com.tangem.data.swap
import arrow.core.right
import com.squareup.moshi.Moshi
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.swap.converter.SwapDataConverter
@ -21,12 +22,15 @@ import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.express.models.ExpressProviderType
import com.tangem.domain.express.models.ExpressRateType
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.NetworkStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteStatusProducer
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.swap.SwapRepositoryV2
import com.tangem.domain.swap.models.*
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
@ -44,8 +48,10 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
private val expressRepository: ExpressRepository,
private val coroutineDispatcher: CoroutineDispatcherProvider,
private val appPreferencesStore: AppPreferencesStore,
private val currencyStatusOperations: BaseCurrencyStatusOperations,
private val dataSignatureVerifier: DataSignatureVerifier,
private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
private val singleQuoteStatusFetcher: SingleQuoteStatusFetcher,
private val currencyStatusProxyCreator: CurrencyStatusProxyCreator,
@NetworkMoshi moshi: Moshi,
) : SwapRepositoryV2 {
@ -102,10 +108,11 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
}.awaitAll().filterNotNull()
}
override suspend fun getPairsOnly(
override suspend fun getSupportedPairs(
userWallet: UserWallet,
initialCurrency: CryptoCurrency,
cryptoCurrencyList: List<CryptoCurrency>,
filterProviderTypes: List<ExpressProviderType>,
): List<SwapPairModel> = withContext(coroutineDispatcher.io) {
val allPairs = getPairsInternal(
userWallet = userWallet,
@ -113,6 +120,12 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
cryptoCurrencyList = cryptoCurrencyList,
)
val providers = expressRepository.getProviders(
userWallet = userWallet,
filterProviderTypes = filterProviderTypes,
)
val mappedProviders = providers.associateBy(ExpressProvider::providerId)
allPairs.map { pair ->
async {
val statusFromDeferred = async {
@ -130,11 +143,20 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
}
}
createPairModelOnly(
currencyFrom = statusFromDeferred.await(),
currencyTo = statusToDeferred.await(),
userWalletId = userWallet.walletId,
)
val currencyStatusFrom = createSendWithSwapCryptoCurrencyStatus(statusFromDeferred.await())
val currencyStatusTo = createSendWithSwapCryptoCurrencyStatus(statusToDeferred.await())
if (currencyStatusFrom != null && currencyStatusTo != null) {
SwapPairModel(
from = currencyStatusFrom,
to = currencyStatusTo,
providers = pair.providers.mapNotNull {
mappedProviders[it.providerId]
},
)
} else {
null
}
}
}.awaitAll().filterNotNull()
}
@ -176,15 +198,14 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
override suspend fun getSwapData(
userWallet: UserWallet,
fromCryptoCurrencyStatus: CryptoCurrencyStatus,
toCryptoCurrencyStatus: CryptoCurrencyStatus,
toCryptoCurrency: CryptoCurrency,
fromAmount: String,
toAddress: String?,
toAddress: String,
expressProvider: ExpressProvider,
rateType: ExpressRateType,
): SwapDataModel = withContext(coroutineDispatcher.io) {
val requestId = UUID.randomUUID().toString()
val fromCryptoCurrency = fromCryptoCurrencyStatus.currency
val toCryptoCurrency = toCryptoCurrencyStatus.currency
val refundData = when (expressProvider.type) {
ExpressProviderType.CEX,
@ -203,7 +224,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
fromNetwork = fromCryptoCurrency.network.backendId,
toNetwork = toCryptoCurrency.network.backendId,
fromAddress = fromCryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value.orEmpty(),
toAddress = toAddress ?: toCryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value.orEmpty(),
toAddress = toAddress,
fromDecimals = fromCryptoCurrency.decimals,
toDecimals = toCryptoCurrency.decimals,
fromAmount = fromAmount,
@ -329,33 +350,38 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
},
)
private suspend fun createPairModelOnly(
currencyFrom: CryptoCurrency?,
currencyTo: CryptoCurrency?,
userWalletId: UserWalletId,
): SwapPairModel? {
return if (currencyFrom != null && currencyTo != null) {
val statusFrom = currencyStatusOperations.getCurrencyStatusSync(
userWalletId = userWalletId,
cryptoCurrencyId = currencyFrom.id,
).getOrNull()
val statusTo = currencyStatusOperations.getCurrencyStatusSync(
userWalletId = userWalletId,
cryptoCurrencyId = currencyTo.id,
).getOrNull()
/**
* Send with swap specific currency status creation
* We support sending to any available to swap and supported network
* It is possible that currency not added to wallet so we ignore network status
*/
private suspend fun createSendWithSwapCryptoCurrencyStatus(cryptoCurrency: CryptoCurrency?): CryptoCurrencyStatus? {
val rawCurrencyId = cryptoCurrency?.id?.rawCurrencyId ?: return null
if (statusFrom != null && statusTo != null) {
SwapPairModel(
from = statusFrom,
to = statusTo,
providers = emptyList(),
)
} else {
null
}
} else {
null
val quote = singleQuoteStatusSupplier.getSyncOrNull(
params = SingleQuoteStatusProducer.Params(rawCurrencyId = rawCurrencyId),
)?.right()
if (quote == null) {
singleQuoteStatusFetcher.invoke(
params = SingleQuoteStatusFetcher.Params(
rawCurrencyId = rawCurrencyId,
appCurrencyId = null,
),
)
}
return currencyStatusProxyCreator.createCurrencyStatus(
currency = cryptoCurrency,
maybeQuoteStatus = quote ?: singleQuoteStatusSupplier.getSyncOrNull(
params = SingleQuoteStatusProducer.Params(rawCurrencyId = rawCurrencyId),
).right(),
maybeNetworkStatus = NetworkStatus(
network = cryptoCurrency.network,
value = NetworkStatus.MissedDerivation, // Caution!!! Do not change this status
).right(),
maybeYieldBalance = null,
).getOrNull()
}
private fun parseTxDetails(txDetailsJson: String): TxDetails? {

View file

@ -12,10 +12,13 @@ import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.express.ExpressRepository
import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.swap.SwapErrorResolver
import com.tangem.domain.swap.SwapRepositoryV2
import com.tangem.domain.swap.SwapTransactionRepository
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -43,8 +46,10 @@ internal object SwapDataModule {
expressRepository: ExpressRepository,
coroutineDispatcher: CoroutineDispatcherProvider,
appPreferencesStore: AppPreferencesStore,
currencyStatusOperations: BaseCurrencyStatusOperations,
dataSignatureVerifier: DataSignatureVerifier,
singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
singleQuoteStatusFetcher: SingleQuoteStatusFetcher,
stakingRepository: StakingRepository,
@NetworkMoshi moshi: Moshi,
): SwapRepositoryV2 {
return DefaultSwapRepositoryV2(
@ -52,9 +57,11 @@ internal object SwapDataModule {
expressRepository = expressRepository,
coroutineDispatcher = coroutineDispatcher,
appPreferencesStore = appPreferencesStore,
currencyStatusOperations = currencyStatusOperations,
dataSignatureVerifier = dataSignatureVerifier,
moshi = moshi,
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
singleQuoteStatusFetcher = singleQuoteStatusFetcher,
currencyStatusProxyCreator = CurrencyStatusProxyCreator(stakingRepository),
)
}

View file

@ -4,12 +4,12 @@ import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.express.models.ExpressProviderType
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.models.SwapDataModel
import com.tangem.domain.swap.models.SwapPairModel
import com.tangem.domain.swap.models.SwapQuoteModel
import com.tangem.domain.swap.models.SwapStatusModel
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import java.math.BigDecimal
/**
@ -21,10 +21,10 @@ interface SwapRepositoryV2 {
/**
* Express swap pairs, both direct and reversed
*
* @param userWallet selected user wallet
* @param initialCurrency currency being swapped (either to or from)
* @param cryptoCurrencyStatusList list of currencies might be swapped
* @param filterProviderTypes filters only specified provider types, if empty returns providers as is
* @param userWallet selected user wallet
* @param initialCurrency currency being swapped (either to or from)
* @param cryptoCurrencyStatusList list of currencies might be swapped
* @param filterProviderTypes filters only specified provider types, if empty returns providers as is
*/
suspend fun getPairs(
userWallet: UserWallet,
@ -33,22 +33,27 @@ interface SwapRepositoryV2 {
filterProviderTypes: List<ExpressProviderType>,
): List<SwapPairModel>
/** Express getPairs request variant without providers request */
suspend fun getPairsOnly(
/**
* Express getPairs request variant for all pairs of crypto currency supported by Tangem Wallet
* Therefore return list may and will include currencies not added to user wallet.
* Used in [Send With Swap]
*/
suspend fun getSupportedPairs(
userWallet: UserWallet,
initialCurrency: CryptoCurrency,
cryptoCurrencyList: List<CryptoCurrency>,
filterProviderTypes: List<ExpressProviderType>,
): List<SwapPairModel>
/**
* Returns swap quotes on selected pair
*
* @param userWallet selected user wallet
* @param fromCryptoCurrency currency being swapped from
* @param toCryptoCurrency currency being swapped to
* @param fromAmount swap amount
* @param provider selected express provider
* @param rateType rate type
* @param userWallet selected user wallet
* @param fromCryptoCurrency currency being swapped from
* @param toCryptoCurrency currency being swapped to
* @param fromAmount swap amount
* @param provider selected express provider
* @param rateType rate type
*/
suspend fun getSwapQuote(
userWallet: UserWallet,
@ -62,20 +67,20 @@ interface SwapRepositoryV2 {
/**
* Returns swap data [SwapDataModel] ready to sign and send on selected quote
*
* @param userWallet selected user wallet
* @param fromCryptoCurrencyStatus currency status being swapped from
* @param toCryptoCurrencyStatus currency status being swapped to
* @param fromAmount swap amount
* @param toAddress destination address (optional, if null send to self)
* @param expressProvider selected swap provider
* @param rateType selected provider rate type
* @param userWallet selected user wallet
* @param fromCryptoCurrencyStatus currency status being swapped from
* @param toCryptoCurrency currency being swapped to
* @param fromAmount swap amount
* @param toAddress destination address
* @param expressProvider selected swap provider
* @param rateType selected provider rate type
*/
suspend fun getSwapData(
userWallet: UserWallet,
fromCryptoCurrencyStatus: CryptoCurrencyStatus,
toCryptoCurrencyStatus: CryptoCurrencyStatus,
toCryptoCurrency: CryptoCurrency,
fromAmount: String,
toAddress: String?,
toAddress: String,
expressProvider: ExpressProvider,
rateType: ExpressRateType,
): SwapDataModel
@ -83,12 +88,12 @@ interface SwapRepositoryV2 {
/**
* Send ExpressApi info that swap transaction occurred
*
* @param userWallet selected user wallet
* @param fromCryptoCurrencyStatus currency status being swapped from
* @param toAddress swap destination address
* @param txId transaction id in ExpressApi
* @param txHash transaction hash in blockchain
* @param txExtraId extra transaction id in ExpressApi
* @param userWallet selected user wallet
* @param fromCryptoCurrencyStatus currency status being swapped from
* @param toAddress swap destination address
* @param txId transaction id in ExpressApi
* @param txHash transaction hash in blockchain
* @param txExtraId extra transaction id in ExpressApi
*/
suspend fun swapTransactionSent(
userWallet: UserWallet,
@ -102,8 +107,8 @@ interface SwapRepositoryV2 {
/**
* Returns status [SwapStatusModel] on active swap
*
* @param userWallet selected user wallet
* @param txId transaction id in ExpressApi
* @param userWallet selected user wallet
* @param txId transaction id in ExpressApi
*/
suspend fun getExchangeStatus(userWallet: UserWallet, txId: String): SwapStatusModel
}

View file

@ -1,7 +1,9 @@
package com.tangem.domain.swap.usecase
import arrow.core.Either
import com.tangem.domain.express.models.ExpressProviderType
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.SwapCryptoCurrency
@ -9,7 +11,6 @@ import com.tangem.domain.swap.models.SwapCurrencies
import com.tangem.domain.swap.models.SwapCurrenciesGroup
import com.tangem.domain.swap.models.SwapPairModel
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
/**
* Returns pais
@ -23,11 +24,13 @@ class GetSwapSupportedPairsUseCase(
userWallet: UserWallet,
initialCurrency: CryptoCurrency,
cryptoCurrencyList: List<CryptoCurrency>,
filterProviderTypes: List<ExpressProviderType>,
) = Either.catch {
val pairs = swapRepositoryV2.getPairsOnly(
val pairs = swapRepositoryV2.getSupportedPairs(
userWallet = userWallet,
initialCurrency = initialCurrency,
cryptoCurrencyList = cryptoCurrencyList,
filterProviderTypes = filterProviderTypes,
)
val filteredOutInitial = cryptoCurrencyList.filterNot { it.id == initialCurrency.id }
@ -74,7 +77,7 @@ class GetSwapSupportedPairsUseCase(
SwapCryptoCurrency(
CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Loading, // todo select token unavailable
value = CryptoCurrencyStatus.Loading,
),
emptyList(),
)

View file

@ -31,6 +31,7 @@ dependencies {
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.swap.models)
/* AndroidX */
implementation(deps.androidx.activity.compose)

View file

@ -10,9 +10,9 @@ import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.swap.models.SwapDirection
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.features.swap.v2.impl.R
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType

View file

@ -18,6 +18,7 @@ import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapCho
import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model.SwapChooseTokenFactory.getErrorMessage
import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model.transformers.SwapChooseContentStateTransformer
import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model.transformers.SwapChooseErrorStateTransformer
import com.tangem.features.swap.v2.impl.common.SwapUtils.SEND_WITH_SWAP_PROVIDER_TYPES
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.transformer.update
import kotlinx.coroutines.delay
@ -79,6 +80,7 @@ internal class SwapChooseTokenNetworkModel @Inject constructor(
userWallet = userWallet,
initialCurrency = params.initialCurrency,
cryptoCurrencyList = cryptoCurrencyList + params.initialCurrency,
filterProviderTypes = SEND_WITH_SWAP_PROVIDER_TYPES,
).getOrElse {
Timber.e(it.toString())
uiState.update(

View file

@ -6,12 +6,16 @@ import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.simple
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.express.models.ExpressProviderType
import com.tangem.features.swap.v2.impl.R
internal object SwapUtils {
const val INCREASE_GAS_LIMIT_FOR_DEX = 112 // 12%
const val INCREASE_GAS_LIMIT_FOR_CEX = 105 // 5%
/** List of supported provider types in Send with Swap */
internal val SEND_WITH_SWAP_PROVIDER_TYPES = listOf(ExpressProviderType.CEX)
fun getExpressErrorMessage(expressError: ExpressError): TextReference {
return when (expressError) {
is ExpressError.InternalError -> resourceReference(

View file

@ -18,7 +18,6 @@ import com.tangem.core.decompose.navigation.inner.InnerRouter
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.decompose.getEmptyComposableContentComponent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.express.models.ExpressProviderType
import com.tangem.domain.swap.models.R
import com.tangem.domain.swap.models.SwapDirection
import com.tangem.features.send.v2.api.subcomponents.destination.DestinationRoute
@ -28,6 +27,7 @@ import com.tangem.features.swap.v2.api.SendWithSwapComponent
import com.tangem.features.swap.v2.impl.amount.SwapAmountComponent
import com.tangem.features.swap.v2.impl.amount.SwapAmountComponentParams
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.common.SwapUtils.SEND_WITH_SWAP_PROVIDER_TYPES
import com.tangem.features.swap.v2.impl.sendviaswap.confirm.SendWithSwapConfirmComponent
import com.tangem.features.swap.v2.impl.sendviaswap.model.SendWithSwapModel
import com.tangem.features.swap.v2.impl.sendviaswap.success.SendWithSwapSuccessComponent
@ -199,9 +199,4 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor(
params: SendWithSwapComponent.Params,
): DefaultSendWithSwapComponent
}
companion object {
/** List of supported provider types in Send with Swap */
internal val SEND_WITH_SWAP_PROVIDER_TYPES = listOf(ExpressProviderType.CEX)
}
}

View file

@ -21,10 +21,10 @@ import com.tangem.features.send.v2.api.subcomponents.destination.SendDestination
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams
import com.tangem.features.swap.v2.impl.amount.SwapAmountBlockComponent
import com.tangem.features.swap.v2.impl.amount.SwapAmountComponentParams
import com.tangem.features.swap.v2.impl.common.SwapUtils.SEND_WITH_SWAP_PROVIDER_TYPES
import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent
import com.tangem.features.swap.v2.impl.sendviaswap.DefaultSendWithSwapComponent.Companion.SEND_WITH_SWAP_PROVIDER_TYPES
import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute
import com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.SendWithSwapConfirmModel
import com.tangem.features.swap.v2.impl.sendviaswap.confirm.ui.SendWithSwapConfirmContent

View file

@ -14,7 +14,6 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.express.models.ExpressProviderType
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase
@ -226,7 +225,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
isAmountSubtractAvailable = isAmountSubtractAvailable,
onExpressError = { expressError ->
swapAlertFactory.getGenericErrorState(
expressError = ExpressError.UnknownError,
expressError = expressError,
onFailedTxEmailClick = {
modelScope.launch {
swapAlertFactory.onFailedTxEmailClick(