Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-19 14:59:17 +05:00
commit 803f0bdfec
11 changed files with 475 additions and 2 deletions

View file

@ -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,
)
}
}

View file

@ -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
}
}
}

View file

@ -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<CryptoCurrencyStatus>,
): List<SwapPairModel> = 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<CryptoCurrency>,
): List<SwapPairModel> = 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<CryptoCurrency>,
) = 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<CryptoCurrency>,
to: List<CryptoCurrency>,
) = 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"
}
}
}

View file

@ -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<CryptoCurrency, LeastTokenInfo> {
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,
)
}
}

View file

@ -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,
)
}
}

View file

@ -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<SwapCryptoCurrency>,
val unavailable: List<SwapCryptoCurrency>,
val afterSearch: Boolean,
)
data class SwapCryptoCurrency(
val currencyStatus: CryptoCurrencyStatus,
val providers: List<ExpressProvider>,
)

View file

@ -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<ExpressProvider>,
)

View file

@ -0,0 +1,6 @@
package com.tangem.domain.swap.models
data class TokenInfo(
val contractAddress: String,
val network: String,
)

View file

@ -0,0 +1,7 @@
package com.tangem.domain.swap
import com.tangem.domain.express.models.ExpressError
interface SwapErrorResolver {
fun resolve(throwable: Throwable): ExpressError
}

View file

@ -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<CryptoCurrencyStatus>,
): List<SwapPairModel>
/** Express getPairs request variant without providers request */
suspend fun getPairsOnly(
userWallet: UserWallet,
initialCurrency: CryptoCurrency,
cryptoCurrencyList: List<CryptoCurrency>,
): List<SwapPairModel>
}

View file

@ -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<CryptoCurrency>,
) = 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<SwapPairModel>.groupPairs(
initialCurrency: CryptoCurrency,
filteringCurrency: (SwapPairModel) -> CryptoCurrencyStatus,
groupingCurrency: (SwapPairModel) -> CryptoCurrencyStatus,
cryptoCurrencyList: List<CryptoCurrency>,
): 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,
)
}
}