diff --git a/app/src/main/java/com/tangem/tap/di/domain/SwapDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SwapDomainModule.kt index a392d9c30e..f0ba15b856 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SwapDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SwapDomainModule.kt @@ -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, + ) + } } \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/converters/error/OnrampErrorConverter.kt b/data/onramp/src/main/java/com/tangem/data/onramp/converters/error/OnrampErrorConverter.kt index 895da30c18..1abd3b4d3d 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/converters/error/OnrampErrorConverter.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/converters/error/OnrampErrorConverter.kt @@ -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, ) : Converter { diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt index 1931f346f2..9adca6af89 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -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, diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt index 6a1f16be09..ef559c44e9 100644 --- a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt @@ -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, val unavailable: List, val afterSearch: Boolean, ) +/** + * Crypto currency with providers to swap + */ data class SwapCryptoCurrency( val currencyStatus: CryptoCurrencyStatus, val providers: List, diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapDirection.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapDirection.kt new file mode 100644 index 0000000000..9c60cc2ec0 --- /dev/null +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapDirection.kt @@ -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, +} \ No newline at end of file diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt new file mode 100644 index 0000000000..a808f07e67 --- /dev/null +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt @@ -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?, +) \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt index b2c768cdc7..7bb2c091ba 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt @@ -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, ): List + + /** + * 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 } \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapPairsUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapPairsUseCase.kt new file mode 100644 index 0000000000..d0a1cd0bf2 --- /dev/null +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapPairsUseCase.kt @@ -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, + ) = 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.groupPairs( + initialCurrency: CryptoCurrency, + filteringCurrency: (SwapPairModel) -> CryptoCurrencyStatus, + groupingCurrency: (SwapPairModel) -> CryptoCurrencyStatus, + cryptoCurrencyStatusList: List, + ): 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, + ) + } +} \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapQuoteUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapQuoteUseCase.kt new file mode 100644 index 0000000000..f0594c8f02 --- /dev/null +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapQuoteUseCase.kt @@ -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 = Either.catch { + swapRepositoryV2.getSwapQuote( + userWallet = userWallet, + fromCryptoCurrency = fromCryptoCurrency, + toCryptoCurrency = toCryptoCurrency, + fromAmount = fromAmount, + provider = provider, + rateType = ExpressRateType.Float, // todo rate type + ) + }.mapLeft(swapErrorResolver::resolve) +} \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SelectInitialPairUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SelectInitialPairUseCase.kt new file mode 100644 index 0000000000..f348bdc52a --- /dev/null +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SelectInitialPairUseCase.kt @@ -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 + } +} \ No newline at end of file