From a692abaffb2f5b7967ce8d6da2ae3ea928192d43 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 18 Jun 2025 18:38:24 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../tangem/tap/di/domain/SwapDomainModule.kt | 19 +- .../data/swap/DefaultSwapErrorResolver.kt | 19 ++ .../data/swap/DefaultSwapRepositoryV2.kt | 200 ++++++++++++++++++ .../data/swap/converter/TokenInfoConverter.kt | 23 ++ .../com/tangem/data/swap/di/SwapDataModule.kt | 52 +++++ .../domain/swap/models/SwapCurrencies.kt | 20 ++ .../domain/swap/models/SwapPairModel.kt | 17 ++ .../tangem/domain/swap/models/TokenInfo.kt | 6 + .../tangem/domain/swap/SwapErrorResolver.kt | 7 + .../tangem/domain/swap/SwapRepositoryV2.kt | 29 +++ .../usecase/GetSwapSupportedPairsUseCase.kt | 85 ++++++++ 11 files changed, 475 insertions(+), 2 deletions(-) create mode 100644 data/swap/src/main/java/com/tangem/data/swap/DefaultSwapErrorResolver.kt create mode 100644 data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt create mode 100644 data/swap/src/main/java/com/tangem/data/swap/converter/TokenInfoConverter.kt create mode 100644 data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt create mode 100644 domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt create mode 100644 domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapPairModel.kt create mode 100644 domain/swap/models/src/main/java/com/tangem/domain/swap/models/TokenInfo.kt create mode 100644 domain/swap/src/main/java/com/tangem/domain/swap/SwapErrorResolver.kt create mode 100644 domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt create mode 100644 domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt 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 e94429b646..a392d9c30e 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 @@ -1,12 +1,15 @@ package com.tangem.tap.di.domain +import com.tangem.domain.swap.SwapErrorResolver +import com.tangem.domain.swap.SwapRepositoryV2 +import com.tangem.domain.swap.usecase.GetSwapSupportedPairsUseCase import com.tangem.feature.swap.domain.GetAvailablePairsUseCase -import com.tangem.feature.swap.domain.api.SwapRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton +import com.tangem.feature.swap.domain.api.SwapRepository as OldSwapRepository /** [REDACTED_AUTHOR] @@ -17,7 +20,19 @@ internal object SwapDomainModule { @Provides @Singleton - fun provideGetAvailablePairsUseCase(swapRepository: SwapRepository): GetAvailablePairsUseCase { + fun provideGetAvailablePairsUseCase(swapRepository: OldSwapRepository): GetAvailablePairsUseCase { return GetAvailablePairsUseCase(swapRepository = swapRepository) } + + @Provides + @Singleton + fun provideGetSwapSupportedPairsUseCase( + swapRepositoryV2: SwapRepositoryV2, + swapErrorResolver: SwapErrorResolver, + ): GetSwapSupportedPairsUseCase { + return GetSwapSupportedPairsUseCase( + swapRepositoryV2 = swapRepositoryV2, + swapErrorResolver = swapErrorResolver, + ) + } } \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapErrorResolver.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapErrorResolver.kt new file mode 100644 index 0000000000..8827a8bc7c --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapErrorResolver.kt @@ -0,0 +1,19 @@ +package com.tangem.data.swap + +import com.tangem.data.express.converter.ExpressErrorConverter +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.domain.express.models.ExpressError +import com.tangem.domain.swap.SwapErrorResolver + +internal class DefaultSwapErrorResolver( + private val expressErrorConverter: ExpressErrorConverter, +) : SwapErrorResolver { + override fun resolve(throwable: Throwable): ExpressError { + return when (throwable) { + is ApiResponseError.HttpException -> { + expressErrorConverter.convert(throwable.errorBody.orEmpty()) + } + else -> ExpressError.UnknownError + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..1931f346f2 --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -0,0 +1,200 @@ +package com.tangem.data.swap + +import com.tangem.data.common.api.safeApiCall +import com.tangem.data.swap.converter.TokenInfoConverter +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.express.TangemExpressApi +import com.tangem.datasource.api.express.models.request.PairsRequestBody +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.models.currency.CryptoCurrency +import com.tangem.domain.swap.SwapRepositoryV2 +import com.tangem.domain.swap.models.SwapPairModel +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject + +@Suppress("LongParameterList") +internal class DefaultSwapRepositoryV2 @Inject constructor( + private val tangemExpressApi: TangemExpressApi, + private val expressRepository: ExpressRepository, + private val coroutineDispatcher: CoroutineDispatcherProvider, + private val appPreferencesStore: AppPreferencesStore, + private val currencyStatusOperations: BaseCurrencyStatusOperations, +) : SwapRepositoryV2 { + + private val tokenInfoConverter = TokenInfoConverter() + override suspend fun getPairs( + userWallet: UserWallet, + initialCurrency: CryptoCurrency, + cryptoCurrencyStatusList: List, + ): List = withContext(coroutineDispatcher.io) { + val cryptoCurrencyList = cryptoCurrencyStatusList.map { it.currency } + + val allPairs = getPairsInternal( + userWallet = userWallet, + initialCurrency = initialCurrency, + cryptoCurrencyList = cryptoCurrencyList, + ) + + val providers = expressRepository.getProviders(userWallet = userWallet) + val mappedProviders = providers.associateBy(ExpressProvider::providerId) + + allPairs.map { pair -> + async { + val statusFrom = cryptoCurrencyStatusList + .firstOrNull { + it.currency.getContractAddress() == pair.from.contractAddress && + it.currency.network.backendId == pair.from.network + } + val statusTo = cryptoCurrencyStatusList + .firstOrNull { + it.currency.getContractAddress() == pair.to.contractAddress && + it.currency.network.backendId == pair.to.network + } + + if (statusFrom != null && statusTo != null) { + SwapPairModel( + from = statusFrom, + to = statusTo, + providers = pair.providers.mapNotNull { + mappedProviders[it.providerId] + }, + ) + } else { + null + } + } + }.awaitAll().filterNotNull() + } + + override suspend fun getPairsOnly( + userWallet: UserWallet, + initialCurrency: CryptoCurrency, + cryptoCurrencyList: List, + ): List = withContext(coroutineDispatcher.io) { + val allPairs = getPairsInternal( + userWallet = userWallet, + initialCurrency = initialCurrency, + cryptoCurrencyList = cryptoCurrencyList, + ) + + allPairs.map { pair -> + async { + val statusFromDeferred = async { + cryptoCurrencyList + .firstOrNull { + it.getContractAddress() == pair.from.contractAddress && + it.network.backendId == pair.from.network + } + } + val statusToDeferred = async { + cryptoCurrencyList + .firstOrNull { + it.getContractAddress() == pair.to.contractAddress && + it.network.backendId == pair.to.network + } + } + + createPairModelOnly( + currencyFrom = statusFromDeferred.await(), + currencyTo = statusToDeferred.await(), + userWalletId = userWallet.walletId, + ) + } + }.awaitAll().filterNotNull() + } + + private suspend fun CoroutineScope.getPairsInternal( + userWallet: UserWallet, + initialCurrency: CryptoCurrency, + cryptoCurrencyList: List, + ) = awaitAll( + // original pairs + async { + invokePairRequest( + userWallet = userWallet, + from = arrayListOf(initialCurrency), + to = cryptoCurrencyList, + ) + }, + // reversed pairs + async { + invokePairRequest( + userWallet = userWallet, + from = cryptoCurrencyList, + to = arrayListOf(initialCurrency), + ) + }, + ).flatten() + + private suspend fun invokePairRequest( + userWallet: UserWallet, + from: List, + to: List, + ) = safeApiCall( + call = { + tangemExpressApi.getPairs( + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), + body = PairsRequestBody( + from = tokenInfoConverter.convertList(from), + to = tokenInfoConverter.convertList(to), + ), + ).getOrThrow() + }, + onError = { + Timber.w(it, "Unable to get pairs") + throw it + }, + ) + + private suspend fun CoroutineScope.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() + + if (statusFrom != null && statusTo != null) { + SwapPairModel( + from = statusFrom, + to = statusTo, + providers = emptyList(), + ) + } else { + null + } + } else { + null + } + } + + private fun CryptoCurrency.getContractAddress(): String { + return when (this) { + is CryptoCurrency.Token -> this.contractAddress + is CryptoCurrency.Coin -> "0" + } + } +} \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/converter/TokenInfoConverter.kt b/data/swap/src/main/java/com/tangem/data/swap/converter/TokenInfoConverter.kt new file mode 100644 index 0000000000..04e3203f28 --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/converter/TokenInfoConverter.kt @@ -0,0 +1,23 @@ +package com.tangem.data.swap.converter + +import com.tangem.datasource.api.express.models.request.LeastTokenInfo +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.swap.models.TokenInfo +import com.tangem.utils.converter.Converter + +class TokenInfoConverter : Converter { + + override fun convert(value: CryptoCurrency): LeastTokenInfo { + return LeastTokenInfo( + contractAddress = (value as? CryptoCurrency.Token)?.contractAddress ?: "0", + network = value.network.backendId, + ) + } + + fun convert(value: TokenInfo): LeastTokenInfo { + return LeastTokenInfo( + contractAddress = value.contractAddress, + network = value.network, + ) + } +} \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt b/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt new file mode 100644 index 0000000000..0ee1971b25 --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt @@ -0,0 +1,52 @@ +package com.tangem.data.swap.di + +import com.squareup.moshi.Moshi +import com.tangem.data.express.converter.ExpressErrorConverter +import com.tangem.data.swap.DefaultSwapErrorResolver +import com.tangem.data.swap.DefaultSwapRepositoryV2 +import com.tangem.datasource.api.express.TangemExpressApi +import com.tangem.datasource.api.express.models.response.ExpressErrorResponse +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.domain.express.ExpressRepository +import com.tangem.domain.swap.SwapErrorResolver +import com.tangem.domain.swap.SwapRepositoryV2 +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +internal object SwapDataModule { + + @Provides + @Singleton + fun provideSwapErrorResolver(@NetworkMoshi moshi: Moshi): SwapErrorResolver { + val jsonAdapter = moshi.adapter(ExpressErrorResponse::class.java) + return DefaultSwapErrorResolver( + ExpressErrorConverter(jsonAdapter), + ) + } + + @Provides + @Singleton + fun provideSwapRepository( + tangemExpressApi: TangemExpressApi, + expressRepository: ExpressRepository, + coroutineDispatcher: CoroutineDispatcherProvider, + appPreferencesStore: AppPreferencesStore, + currencyStatusOperations: BaseCurrencyStatusOperations, + ): SwapRepositoryV2 { + return DefaultSwapRepositoryV2( + tangemExpressApi = tangemExpressApi, + expressRepository = expressRepository, + coroutineDispatcher = coroutineDispatcher, + appPreferencesStore = appPreferencesStore, + currencyStatusOperations = currencyStatusOperations, + ) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..6a1f16be09 --- /dev/null +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.swap.models + +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.tokens.model.CryptoCurrencyStatus + +data class SwapCurrencies( + val fromGroup: SwapCurrenciesGroup, + val toGroup: SwapCurrenciesGroup, +) + +data class SwapCurrenciesGroup( + val available: List, + val unavailable: List, + val afterSearch: Boolean, +) + +data class SwapCryptoCurrency( + val currencyStatus: CryptoCurrencyStatus, + val providers: List, +) \ No newline at end of file diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapPairModel.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapPairModel.kt new file mode 100644 index 0000000000..5e8e0479f9 --- /dev/null +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapPairModel.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.swap.models + +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.tokens.model.CryptoCurrencyStatus + +/** + * Domain layer representation of SwapPair data network model. + * + * @property from Short information about the token we want to change + * @property to Short information about the token we want to exchange for + * @property providers Exchange providers + */ +data class SwapPairModel( + val from: CryptoCurrencyStatus, + val to: CryptoCurrencyStatus, + val providers: List, +) \ No newline at end of file diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/TokenInfo.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/TokenInfo.kt new file mode 100644 index 0000000000..9bd09cc984 --- /dev/null +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/TokenInfo.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.swap.models + +data class TokenInfo( + val contractAddress: String, + val network: String, +) \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/SwapErrorResolver.kt b/domain/swap/src/main/java/com/tangem/domain/swap/SwapErrorResolver.kt new file mode 100644 index 0000000000..b9f75cb5c1 --- /dev/null +++ b/domain/swap/src/main/java/com/tangem/domain/swap/SwapErrorResolver.kt @@ -0,0 +1,7 @@ +package com.tangem.domain.swap + +import com.tangem.domain.express.models.ExpressError + +interface SwapErrorResolver { + fun resolve(throwable: Throwable): ExpressError +} \ 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 new file mode 100644 index 0000000000..b2c768cdc7 --- /dev/null +++ b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt @@ -0,0 +1,29 @@ +package com.tangem.domain.swap + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.swap.models.SwapPairModel +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet + +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 + */ + suspend fun getPairs( + userWallet: UserWallet, + initialCurrency: CryptoCurrency, + cryptoCurrencyStatusList: List, + ): List + + /** Express getPairs request variant without providers request */ + suspend fun getPairsOnly( + userWallet: UserWallet, + initialCurrency: CryptoCurrency, + cryptoCurrencyList: List, + ): List +} \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt new file mode 100644 index 0000000000..1954bc4f85 --- /dev/null +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt @@ -0,0 +1,85 @@ +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 + +/** + * Returns pais + */ +class GetSwapSupportedPairsUseCase( + private val swapRepositoryV2: SwapRepositoryV2, + private val swapErrorResolver: SwapErrorResolver, +) { + + suspend operator fun invoke( + userWallet: UserWallet, + initialCurrency: CryptoCurrency, + cryptoCurrencyList: List, + ) = Either.catch { + val pairs = swapRepositoryV2.getPairsOnly( + userWallet = userWallet, + initialCurrency = initialCurrency, + cryptoCurrencyList = cryptoCurrencyList, + ) + + val filteredOutInitial = cryptoCurrencyList.filterNot { it.id == initialCurrency.id } + + val fromGroup = pairs.groupPairs( + initialCurrency = initialCurrency, + filteringCurrency = { it.from }, + groupingCurrency = { it.to }, + cryptoCurrencyList = filteredOutInitial, + ) + val toGroup = pairs.groupPairs( + initialCurrency = initialCurrency, + filteringCurrency = { it.to }, + groupingCurrency = { it.from }, + cryptoCurrencyList = filteredOutInitial, + ) + + SwapCurrencies( + fromGroup = fromGroup, + toGroup = toGroup, + ) + }.mapLeft(swapErrorResolver::resolve) + + private fun List.groupPairs( + initialCurrency: CryptoCurrency, + filteringCurrency: (SwapPairModel) -> CryptoCurrencyStatus, + groupingCurrency: (SwapPairModel) -> CryptoCurrencyStatus, + cryptoCurrencyList: List, + ): SwapCurrenciesGroup { + val availableCryptoCurrencies = filter { pair -> filteringCurrency(pair).currency.id == initialCurrency.id } + // Search available to swap currency + .filter { pair -> + cryptoCurrencyList.any { currencyStatus -> + currencyStatus.id == pair.to.currency.id + } + }.map { pair -> SwapCryptoCurrency(groupingCurrency(pair), pair.providers) } + + val unavailableCryptoCurrencies = + cryptoCurrencyList - availableCryptoCurrencies.map { it.currencyStatus.currency }.toSet() + + return SwapCurrenciesGroup( + available = availableCryptoCurrencies, + unavailable = unavailableCryptoCurrencies.map { currency -> + SwapCryptoCurrency( + CryptoCurrencyStatus( + currency = currency, + value = CryptoCurrencyStatus.Loading, // todo select token unavailable + ), + emptyList(), + ) + }, + afterSearch = false, + ) + } +} \ No newline at end of file