Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-24 16:31:35 +05:00
parent 3e2de9fd6b
commit 357955b1dd
10 changed files with 359 additions and 0 deletions

View file

@ -2,7 +2,11 @@ package com.tangem.tap.di.domain
import com.tangem.domain.swap.SwapErrorResolver
import com.tangem.domain.swap.SwapRepositoryV2
import com.tangem.domain.swap.SwapTransactionRepository
import com.tangem.domain.swap.usecase.GetSwapPairsUseCase
import com.tangem.domain.swap.usecase.GetSwapQuoteUseCase
import com.tangem.domain.swap.usecase.GetSwapSupportedPairsUseCase
import com.tangem.domain.swap.usecase.SelectInitialPairUseCase
import com.tangem.feature.swap.domain.GetAvailablePairsUseCase
import dagger.Module
import dagger.Provides
@ -35,4 +39,38 @@ internal object SwapDomainModule {
swapErrorResolver = swapErrorResolver,
)
}
@Provides
@Singleton
fun provideGetSwapPairsUseCase(
swapRepositoryV2: SwapRepositoryV2,
swapErrorResolver: SwapErrorResolver,
): GetSwapPairsUseCase {
return GetSwapPairsUseCase(
swapRepositoryV2 = swapRepositoryV2,
swapErrorResolver = swapErrorResolver,
)
}
@Provides
@Singleton
fun provideSelectInitialPairUseCase(
swapTransactionRepository: SwapTransactionRepository,
): SelectInitialPairUseCase {
return SelectInitialPairUseCase(
swapTransactionRepository = swapTransactionRepository,
)
}
@Provides
@Singleton
fun provideGetSwapQuoteUseCase(
swapRepositoryV2: SwapRepositoryV2,
swapErrorResolver: SwapErrorResolver,
): GetSwapQuoteUseCase {
return GetSwapQuoteUseCase(
swapRepositoryV2 = swapRepositoryV2,
swapErrorResolver = swapErrorResolver,
)
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.utils.converter.Converter
@Deprecated("Use ExpressErrorConverter")
internal class OnrampErrorConverter(
private val jsonAdapter: JsonAdapter<ExpressErrorResponse>,
) : Converter<String, OnrampError> {

View file

@ -9,9 +9,11 @@ import com.tangem.datasource.exchangeservice.swap.ExpressUtils
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.express.ExpressRepository
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.swap.SwapRepositoryV2
import com.tangem.domain.swap.models.SwapPairModel
import com.tangem.domain.swap.models.SwapQuoteModel
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.wallets.models.UserWallet
@ -22,6 +24,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
@Suppress("LongParameterList")
@ -34,6 +37,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
) : SwapRepositoryV2 {
private val tokenInfoConverter = TokenInfoConverter()
override suspend fun getPairs(
userWallet: UserWallet,
initialCurrency: CryptoCurrency,
@ -115,6 +119,40 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
}.awaitAll().filterNotNull()
}
override suspend fun getSwapQuote(
userWallet: UserWallet,
fromCryptoCurrency: CryptoCurrency,
toCryptoCurrency: CryptoCurrency,
fromAmount: BigDecimal,
provider: ExpressProvider,
rateType: ExpressRateType,
): SwapQuoteModel = withContext(coroutineDispatcher.io) {
val response = tangemExpressApi.getExchangeQuote(
fromAmount = fromAmount.movePointRight(fromCryptoCurrency.decimals).toString(),
fromNetwork = fromCryptoCurrency.network.backendId,
fromContractAddress = fromCryptoCurrency.getContractAddress(),
fromDecimals = fromCryptoCurrency.decimals,
toNetwork = toCryptoCurrency.network.backendId,
toContractAddress = toCryptoCurrency.getContractAddress(),
toDecimals = toCryptoCurrency.decimals,
providerId = provider.providerId,
rateType = rateType.name.lowercase(),
userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode(
userWallet = userWallet,
appPreferencesStore = appPreferencesStore,
),
).getOrThrow()
val toTokenAmount = requireNotNull(response.toAmount.toBigDecimalOrNull()?.movePointLeft(response.toDecimals))
return@withContext SwapQuoteModel(
provider = provider,
toTokenAmount = toTokenAmount,
allowanceContract = response.allowanceContract,
)
}
private suspend fun CoroutineScope.getPairsInternal(
userWallet: UserWallet,
initialCurrency: CryptoCurrency,

View file

@ -3,17 +3,43 @@ package com.tangem.domain.swap.models
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
/**
* Model of currencies available to swap
*
* @param fromGroup group of currencies available swap from
* @param toGroup group of currencies available swap to
*/
data class SwapCurrencies(
val fromGroup: SwapCurrenciesGroup,
val toGroup: SwapCurrenciesGroup,
)
/**
* Return swap group depending on [swapDirection]
*/
fun SwapCurrencies.getGroupWithReverse(swapDirection: SwapDirection): SwapCurrenciesGroup {
return when (swapDirection) {
SwapDirection.Reverse -> fromGroup
SwapDirection.Direct -> toGroup
}
}
/**
* Swap group model
*
* @param available list of available currencies to swap
* @param available list of unavailable currencies to swap
* @param afterSearch flag indicates whether user searched token
*/
data class SwapCurrenciesGroup(
val available: List<SwapCryptoCurrency>,
val unavailable: List<SwapCryptoCurrency>,
val afterSearch: Boolean,
)
/**
* Crypto currency with providers to swap
*/
data class SwapCryptoCurrency(
val currencyStatus: CryptoCurrencyStatus,
val providers: List<ExpressProvider>,

View file

@ -0,0 +1,16 @@
package com.tangem.domain.swap.models
import com.tangem.domain.swap.models.SwapDirection.Direct
import com.tangem.domain.swap.models.SwapDirection.Reverse
/**
* Swap direction
*
* Initial currency can be swap to or from.
* If swap being swap from direction is [Direct],
* Otherwise direction is [Reverse]
*/
enum class SwapDirection {
Direct,
Reverse,
}

View file

@ -0,0 +1,17 @@
package com.tangem.domain.swap.models
import com.tangem.domain.express.models.ExpressProvider
import java.math.BigDecimal
/**
* Quote model holds data about current amounts of swaps and fees
*
* @property provider swap provider
* @property toTokenAmount amount of token you want to receive
* @property allowanceContract whether swap occurs via third token
*/
data class SwapQuoteModel(
val provider: ExpressProvider,
val toTokenAmount: BigDecimal,
val allowanceContract: String?,
)

View file

@ -1,10 +1,17 @@
package com.tangem.domain.swap
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.swap.models.SwapPairModel
import com.tangem.domain.swap.models.SwapQuoteModel
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import java.math.BigDecimal
/**
* Swap repository
*/
interface SwapRepositoryV2 {
/**
@ -26,4 +33,24 @@ interface SwapRepositoryV2 {
initialCurrency: CryptoCurrency,
cryptoCurrencyList: List<CryptoCurrency>,
): 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
*/
@Suppress("LongParameterList")
suspend fun getSwapQuote(
userWallet: UserWallet,
fromCryptoCurrency: CryptoCurrency,
toCryptoCurrency: CryptoCurrency,
fromAmount: BigDecimal,
provider: ExpressProvider,
rateType: ExpressRateType,
): SwapQuoteModel
}

View file

@ -0,0 +1,80 @@
package com.tangem.domain.swap.usecase
import arrow.core.Either
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.swap.SwapErrorResolver
import com.tangem.domain.swap.SwapRepositoryV2
import com.tangem.domain.swap.models.SwapCryptoCurrency
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.wallets.models.UserWallet
/**
* Get list of swap pairs
*/
class GetSwapPairsUseCase(
private val swapRepositoryV2: SwapRepositoryV2,
private val swapErrorResolver: SwapErrorResolver,
) {
/**
* @param userWallet selected user wallet
* @param initialCurrency initial currency to swap (to or from)
* @param cryptoCurrencyStatusList list of added cryptocurrencies
*/
suspend operator fun invoke(
userWallet: UserWallet,
initialCurrency: CryptoCurrency,
cryptoCurrencyStatusList: List<CryptoCurrencyStatus>,
) = Either.catch {
val pairs = swapRepositoryV2.getPairs(
userWallet = userWallet,
initialCurrency = initialCurrency,
cryptoCurrencyStatusList = cryptoCurrencyStatusList,
)
val fromGroup = pairs.groupPairs(
initialCurrency = initialCurrency,
filteringCurrency = { it.from },
groupingCurrency = { it.to },
cryptoCurrencyStatusList = cryptoCurrencyStatusList,
)
val toGroup = pairs.groupPairs(
initialCurrency = initialCurrency,
filteringCurrency = { it.to },
groupingCurrency = { it.from },
cryptoCurrencyStatusList = cryptoCurrencyStatusList,
)
SwapCurrencies(
fromGroup = fromGroup,
toGroup = toGroup,
)
}.mapLeft(swapErrorResolver::resolve)
private fun List<SwapPairModel>.groupPairs(
initialCurrency: CryptoCurrency,
filteringCurrency: (SwapPairModel) -> CryptoCurrencyStatus,
groupingCurrency: (SwapPairModel) -> CryptoCurrencyStatus,
cryptoCurrencyStatusList: List<CryptoCurrencyStatus>,
): SwapCurrenciesGroup {
val availableCryptoCurrencies = filter { pair -> filteringCurrency(pair).currency.id == initialCurrency.id }
// Search available to swap currency
.filter { pair ->
cryptoCurrencyStatusList.any { currencyStatus ->
currencyStatus.currency.id == pair.to.currency.id
}
}.map { pair -> SwapCryptoCurrency(groupingCurrency(pair), pair.providers) }
val unavailableCryptoCurrencies =
cryptoCurrencyStatusList - availableCryptoCurrencies.map { it.currencyStatus }.toSet()
return SwapCurrenciesGroup(
available = availableCryptoCurrencies,
unavailable = unavailableCryptoCurrencies.map { pair -> SwapCryptoCurrency(pair, emptyList()) },
afterSearch = false,
)
}
}

View file

@ -0,0 +1,45 @@
package com.tangem.domain.swap.usecase
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.swap.SwapErrorResolver
import com.tangem.domain.swap.SwapRepositoryV2
import com.tangem.domain.swap.models.SwapQuoteModel
import com.tangem.domain.wallets.models.UserWallet
import java.math.BigDecimal
/**
* Get swap quote for selected pair
*/
class GetSwapQuoteUseCase(
private val swapRepositoryV2: SwapRepositoryV2,
private val swapErrorResolver: SwapErrorResolver,
) {
/**
* @param userWallet selected user wallet
* @param fromCryptoCurrency currency swap from
* @param toCryptoCurrency currency swap to
* @param fromAmount swap amount
* @param provider swap provider
*/
suspend operator fun invoke(
userWallet: UserWallet,
fromCryptoCurrency: CryptoCurrency,
toCryptoCurrency: CryptoCurrency,
fromAmount: BigDecimal,
provider: ExpressProvider,
): Either<ExpressError, SwapQuoteModel> = Either.catch {
swapRepositoryV2.getSwapQuote(
userWallet = userWallet,
fromCryptoCurrency = fromCryptoCurrency,
toCryptoCurrency = toCryptoCurrency,
fromAmount = fromAmount,
provider = provider,
rateType = ExpressRateType.Float, // todo rate type
)
}.mapLeft(swapErrorResolver::resolve)
}

View file

@ -0,0 +1,71 @@
package com.tangem.domain.swap.usecase
import com.tangem.domain.models.currency.CryptoCurrency
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.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.utils.extensions.orZero
/**
* Select initial pair to swap
*/
class SelectInitialPairUseCase(
private val swapTransactionRepository: SwapTransactionRepository,
) {
/**
* @param userWallet selected user wallet
* @param primaryCryptoCurrency initial currency
* @param secondaryCryptoCurrency selected currency
* @param swapCurrencies list of currencies with providers
* @param swapDirection swap direction
*/
suspend operator fun invoke(
userWallet: UserWallet,
primaryCryptoCurrency: CryptoCurrency,
secondaryCryptoCurrency: CryptoCurrency?,
swapCurrencies: SwapCurrencies,
swapDirection: SwapDirection,
): CryptoCurrencyStatus? {
val swapCurrenciesGroup = swapCurrencies.getGroupWithReverse(swapDirection)
return tryToGetAlreadySelectedCurrency(secondaryCryptoCurrency, swapCurrenciesGroup)
?: tryGetFromCache(userWallet, primaryCryptoCurrency, swapCurrenciesGroup)
?: tryGetWithMaxAmount(swapCurrenciesGroup)
?: swapCurrenciesGroup.available.firstOrNull()?.currencyStatus
}
private fun tryToGetAlreadySelectedCurrency(
secondaryCryptoCurrency: CryptoCurrency?,
swapCurrenciesGroup: SwapCurrenciesGroup,
): CryptoCurrencyStatus? {
return secondaryCryptoCurrency?.let {
swapCurrenciesGroup.available.firstOrNull {
secondaryCryptoCurrency.id == it.currencyStatus.currency.id
}?.currencyStatus
}
}
private suspend fun tryGetFromCache(
userWallet: UserWallet,
primaryCryptoCurrency: CryptoCurrency,
swapCurrenciesGroup: SwapCurrenciesGroup,
): CryptoCurrencyStatus? {
val id = swapTransactionRepository.getLastSwappedCryptoCurrencyId(userWallet.walletId) ?: return null
return if (id != primaryCryptoCurrency.id.value) {
swapCurrenciesGroup.available.find { it.currencyStatus.currency.id.value == id }?.currencyStatus
} else {
null
}
}
private fun tryGetWithMaxAmount(swapCurrenciesGroup: SwapCurrenciesGroup): CryptoCurrencyStatus? {
return swapCurrenciesGroup.available.maxByOrNull {
it.currencyStatus.value.fiatAmount.orZero()
}?.currencyStatus
}
}