Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-18 18:38:24 +05:00
parent 6f64d550cb
commit a692abaffb
11 changed files with 475 additions and 2 deletions

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