Updated on 2026-08-14
This commit is contained in:
parent
44231e988d
commit
e72c2c87bc
28 changed files with 426 additions and 105 deletions
|
|
@ -1,19 +1,12 @@
|
|||
package com.tangem.data.onramp
|
||||
|
||||
import com.tangem.data.onramp.converters.error.OnrampErrorConverter
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.domain.onramp.model.OnrampError
|
||||
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
|
||||
|
||||
// TODO: [REDACTED_JIRA]
|
||||
internal class DefaultOnrampErrorResolver : OnrampErrorResolver {
|
||||
|
||||
private val onrampErrorConverter = OnrampErrorConverter()
|
||||
|
||||
override fun resolve(throwable: Throwable): OnrampError {
|
||||
return if (throwable is ApiResponseError.HttpException) {
|
||||
onrampErrorConverter.convert(throwable.errorBody.orEmpty())
|
||||
} else {
|
||||
OnrampError.UnknownError
|
||||
}
|
||||
return OnrampError.UnknownError
|
||||
}
|
||||
}
|
||||
|
|
@ -5,13 +5,19 @@ import com.tangem.data.onramp.converters.CountryConverter
|
|||
import com.tangem.data.onramp.converters.CurrencyConverter
|
||||
import com.tangem.data.onramp.converters.PaymentMethodConverter
|
||||
import com.tangem.data.onramp.converters.StatusConverter
|
||||
import com.tangem.data.onramp.converters.error.OnrampQuotesErrorConverter
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.express.TangemExpressApi
|
||||
import com.tangem.datasource.api.express.models.TangemExpressValues
|
||||
import com.tangem.datasource.api.express.models.response.ExchangeProvider
|
||||
import com.tangem.datasource.api.onramp.OnrampApi
|
||||
import com.tangem.datasource.api.onramp.models.common.OnrampDestinationDTO
|
||||
import com.tangem.datasource.api.onramp.models.request.OnrampPairsRequest
|
||||
import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO
|
||||
import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO
|
||||
import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO
|
||||
import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO
|
||||
import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore
|
||||
import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore
|
||||
import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore
|
||||
|
|
@ -20,27 +26,30 @@ import com.tangem.datasource.local.preferences.PreferencesKeys
|
|||
import com.tangem.datasource.local.preferences.utils.getObject
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.storeObject
|
||||
import com.tangem.domain.onramp.model.OnrampCountry
|
||||
import com.tangem.domain.onramp.model.OnrampCurrency
|
||||
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||
import com.tangem.domain.onramp.model.OnrampStatus
|
||||
import com.tangem.domain.onramp.model.*
|
||||
import com.tangem.domain.onramp.repositories.OnrampRepository
|
||||
import com.tangem.domain.tokens.model.Amount
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class DefaultOnrampRepository(
|
||||
private val onrampApi: OnrampApi,
|
||||
private val expressApi: TangemExpressApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val paymentMethodsStore: OnrampPaymentMethodsStore,
|
||||
private val pairsStore: OnrampPairsStore,
|
||||
private val quotesStore: OnrampQuotesStore,
|
||||
private val quotesErrorConverter: OnrampQuotesErrorConverter,
|
||||
) : OnrampRepository {
|
||||
|
||||
private val currencyConverter = CurrencyConverter()
|
||||
|
|
@ -120,77 +129,98 @@ internal class DefaultOnrampRepository(
|
|||
throw it
|
||||
},
|
||||
)
|
||||
paymentMethodsStore.store(PAYMENT_METHODS_KEY, response)
|
||||
paymentMethodsStore.store(PAYMENT_METHODS_KEY, response.removeApplePay())
|
||||
}
|
||||
|
||||
override suspend fun fetchPairs(currency: OnrampCurrency, country: OnrampCountry, cryptoCurrency: CryptoCurrency) =
|
||||
withContext(dispatchers.io) {
|
||||
val pairs = safeApiCall(
|
||||
call = {
|
||||
onrampApi.getPairs(
|
||||
body = OnrampPairsRequest(
|
||||
fromCurrencyCode = currency.code,
|
||||
countryCode = country.code,
|
||||
to = listOf(
|
||||
OnrampDestinationDTO(
|
||||
contractAddress = cryptoCurrency.getContractAddress(),
|
||||
network = cryptoCurrency.network.backendId,
|
||||
val onrampPairs = async {
|
||||
safeApiCall(
|
||||
call = {
|
||||
onrampApi.getPairs(
|
||||
body = OnrampPairsRequest(
|
||||
fromCurrencyCode = currency.code,
|
||||
countryCode = country.code,
|
||||
to = listOf(
|
||||
OnrampDestinationDTO(
|
||||
contractAddress = cryptoCurrency.getContractAddress(),
|
||||
network = cryptoCurrency.network.backendId,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
).bind()
|
||||
},
|
||||
onError = {
|
||||
Timber.w(it, "Unable to fetch onramp pairs")
|
||||
throw it
|
||||
},
|
||||
)
|
||||
pairsStore.store(PAIRS_KEY, pairs)
|
||||
).bind()
|
||||
},
|
||||
onError = {
|
||||
Timber.w(it, "Unable to fetch onramp pairs")
|
||||
throw it
|
||||
},
|
||||
)
|
||||
}
|
||||
val providers = async {
|
||||
safeApiCall(
|
||||
call = { expressApi.getProviders().bind() },
|
||||
onError = {
|
||||
Timber.w(it, "Unable to fetch express providers")
|
||||
throw it
|
||||
},
|
||||
)
|
||||
}
|
||||
storeOnrampPairs(pairs = onrampPairs.await(), providers = providers.await())
|
||||
}
|
||||
|
||||
override suspend fun fetchQuotes(
|
||||
currency: OnrampCurrency,
|
||||
country: OnrampCountry,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
fromAmount: String,
|
||||
) = withContext(dispatchers.io) {
|
||||
override suspend fun fetchQuotes(cryptoCurrency: CryptoCurrency, amount: Amount) = withContext(dispatchers.io) {
|
||||
val pairs = requireNotNull(pairsStore.getSyncOrNull(PAIRS_KEY)) {
|
||||
"Unable to get pairs. At this point they must not be null."
|
||||
}
|
||||
val quotes = pairs.flatMap { pair ->
|
||||
val amountValue = requireNotNull(amount.value) { "Amount value must not be null" }
|
||||
val fromAmount = amountValue.movePointRight(amount.decimals).toString()
|
||||
val currency = requireNotNull(getDefaultCurrencySync()) { "Default currency must not be null" }
|
||||
val country = requireNotNull(getDefaultCountrySync()) { "Default country must not be null" }
|
||||
val quotes: List<OnrampQuote> = pairs.flatMap { pair ->
|
||||
pair.providers.flatMap { provider ->
|
||||
provider.paymentMethods.map { paymentMethod ->
|
||||
async {
|
||||
safeApiCall(
|
||||
call = {
|
||||
onrampApi.getQuote(
|
||||
val response = onrampApi.getQuote(
|
||||
fromCurrencyCode = currency.code,
|
||||
toContractAddress = cryptoCurrency.getContractAddress(),
|
||||
toNetwork = cryptoCurrency.network.backendId,
|
||||
paymentMethod = paymentMethod,
|
||||
paymentMethod = paymentMethod.id,
|
||||
countryCode = country.code,
|
||||
fromAmount = fromAmount,
|
||||
toDecimals = cryptoCurrency.decimals,
|
||||
providerId = provider.providerId,
|
||||
providerId = provider.id,
|
||||
).bind()
|
||||
},
|
||||
onError = {
|
||||
Timber.w(
|
||||
it,
|
||||
"Unable to fetch onramp quotes for ${provider.providerId} -> $paymentMethod",
|
||||
OnrampQuote.Content(
|
||||
fromAmount = convertToBigDecimal(response.fromAmount),
|
||||
toAmount = convertToBigDecimal(response.toAmount),
|
||||
minFromAmount = convertToBigDecimal(response.minFromAmount),
|
||||
maxFromAmount = convertToBigDecimal(response.maxFromAmount),
|
||||
paymentMethodId = response.paymentMethod,
|
||||
providerId = response.providerId,
|
||||
)
|
||||
null
|
||||
},
|
||||
onError = { error ->
|
||||
if (error is ApiResponseError.HttpException) {
|
||||
quotesErrorConverter.convert(error.errorBody.orEmpty() to amount)
|
||||
} else {
|
||||
Timber.w(error, "Unable to fetch onramp quotes for ${provider.id}. $error")
|
||||
null
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
.filterNotNull()
|
||||
}.awaitAll().filterNotNull()
|
||||
quotesStore.store(QUOTES_KEY, quotes)
|
||||
}
|
||||
|
||||
override fun getQuotes(): Flow<List<OnrampQuote>> {
|
||||
return quotesStore.get(QUOTES_KEY)
|
||||
}
|
||||
|
||||
override suspend fun getPaymentMethods(): List<OnrampPaymentMethod> {
|
||||
val paymentMethods = requireNotNull(paymentMethodsStore.getSyncOrNull(PAYMENT_METHODS_KEY)) {
|
||||
"Onramp payment methods is absent in storage"
|
||||
|
|
@ -198,17 +228,43 @@ internal class DefaultOnrampRepository(
|
|||
return paymentMethodsConverter.convertList(paymentMethods)
|
||||
}
|
||||
|
||||
override suspend fun clearCache() {
|
||||
override suspend fun clearCache() = withContext(NonCancellable) {
|
||||
paymentMethodsStore.clear()
|
||||
pairsStore.clear()
|
||||
quotesStore.clear()
|
||||
}
|
||||
|
||||
private suspend fun storeOnrampPairs(pairs: List<OnrampPairDTO>, providers: List<ExchangeProvider>) {
|
||||
val onrampPaymentMethods = getPaymentMethods()
|
||||
val onrampPairs = pairs.map { pair ->
|
||||
val onrampProviders = pair.providers.mapNotNull { onrampProviderDTO ->
|
||||
val providerInfo = providers.find { provider ->
|
||||
provider.id == onrampProviderDTO.providerId
|
||||
} ?: return@mapNotNull null
|
||||
|
||||
OnrampProvider(
|
||||
id = onrampProviderDTO.providerId,
|
||||
info = OnrampProviderInfo(name = providerInfo.name, imageLarge = providerInfo.imageLargeUrl),
|
||||
paymentMethods = onrampPaymentMethods.filter { paymentMethod ->
|
||||
onrampProviderDTO.paymentMethods.any { paymentMethod.id == it }
|
||||
},
|
||||
)
|
||||
}
|
||||
OnrampPair(onrampProviders)
|
||||
}
|
||||
pairsStore.store(PAIRS_KEY, onrampPairs)
|
||||
}
|
||||
|
||||
private fun CryptoCurrency.getContractAddress(): String = when (this) {
|
||||
is CryptoCurrency.Coin -> TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
|
||||
is CryptoCurrency.Token -> this.contractAddress
|
||||
}
|
||||
|
||||
private fun convertToBigDecimal(value: String): BigDecimal =
|
||||
requireNotNull(value.toBigDecimalOrNull()) { "Can not parse $value to BigDecimal" }
|
||||
|
||||
private fun List<PaymentMethodDTO>.removeApplePay(): List<PaymentMethodDTO> = filterNot { it.id == "apple-pay" }
|
||||
|
||||
private companion object {
|
||||
const val PAYMENT_METHODS_KEY = "onramp_payment_methods"
|
||||
const val PAIRS_KEY = "onramp_pairs"
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
package com.tangem.data.onramp.converters.error
|
||||
|
||||
import com.tangem.domain.onramp.model.OnrampError
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class OnrampErrorConverter : Converter<String, OnrampError> {
|
||||
|
||||
override fun convert(value: String): OnrampError {
|
||||
return OnrampError.UnknownError
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.tangem.data.onramp.converters.error
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.tangem.datasource.api.express.models.response.ExpressError
|
||||
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
|
||||
import com.tangem.domain.onramp.model.OnrampQuote
|
||||
import com.tangem.domain.tokens.model.Amount
|
||||
import com.tangem.domain.tokens.model.AmountType
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class OnrampQuotesErrorConverter(
|
||||
private val jsonAdapter: JsonAdapter<ExpressErrorResponse>,
|
||||
) : Converter<Pair<String, Amount>, OnrampQuote.Error?> {
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override fun convert(value: Pair<String, Amount>): OnrampQuote.Error? {
|
||||
val (errorResponse, amount) = value
|
||||
try {
|
||||
val error = jsonAdapter.fromJson(errorResponse)?.error ?: return null
|
||||
return when (error.code) {
|
||||
2250 -> tryParseExchangeTooSmallAmountError(error = error, amount = amount)
|
||||
2251 -> tryParseExchangeTooBigAmountError(error = error, amount = amount)
|
||||
else -> null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryParseExchangeTooSmallAmountError(
|
||||
error: ExpressError,
|
||||
amount: Amount,
|
||||
): OnrampQuote.Error.AmountTooSmallError? {
|
||||
val minAmount = error.value?.minAmount ?: return null
|
||||
val decimals = error.value?.decimals ?: return null
|
||||
|
||||
return OnrampQuote.Error.AmountTooSmallError(
|
||||
amount = createFromAmountWithOffset(
|
||||
amountWithOffset = minAmount,
|
||||
decimals = decimals,
|
||||
symbol = amount.currencySymbol,
|
||||
type = amount.type,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun tryParseExchangeTooBigAmountError(
|
||||
error: ExpressError,
|
||||
amount: Amount,
|
||||
): OnrampQuote.Error.AmountTooBigError? {
|
||||
val maxAmount = error.value?.maxAmount ?: return null
|
||||
val decimals = error.value?.decimals ?: return null
|
||||
|
||||
return OnrampQuote.Error.AmountTooBigError(
|
||||
amount = createFromAmountWithOffset(
|
||||
amountWithOffset = maxAmount,
|
||||
decimals = decimals,
|
||||
symbol = amount.currencySymbol,
|
||||
type = amount.type,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createFromAmountWithOffset(
|
||||
amountWithOffset: String,
|
||||
decimals: Int,
|
||||
symbol: String,
|
||||
type: AmountType,
|
||||
): Amount {
|
||||
return Amount(
|
||||
value = requireNotNull(
|
||||
amountWithOffset.toBigDecimalOrNull()?.movePointLeft(decimals),
|
||||
) { "wrong amount format, use only digits" },
|
||||
decimals = decimals,
|
||||
currencySymbol = symbol,
|
||||
type = type,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,14 @@
|
|||
package com.tangem.data.onramp.di
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.data.onramp.DefaultOnrampErrorResolver
|
||||
import com.tangem.data.onramp.DefaultOnrampRepository
|
||||
import com.tangem.data.onramp.DefaultOnrampTransactionRepository
|
||||
import com.tangem.data.onramp.converters.error.OnrampQuotesErrorConverter
|
||||
import com.tangem.datasource.api.express.TangemExpressApi
|
||||
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
|
||||
import com.tangem.datasource.api.onramp.OnrampApi
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore
|
||||
import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore
|
||||
import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore
|
||||
|
|
@ -26,19 +31,23 @@ internal object OnrampDataModule {
|
|||
@Singleton
|
||||
fun provideOnrampRepository(
|
||||
onrampApi: OnrampApi,
|
||||
expressApi: TangemExpressApi,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
paymentMethodsStore: OnrampPaymentMethodsStore,
|
||||
pairsStore: OnrampPairsStore,
|
||||
quotesStore: OnrampQuotesStore,
|
||||
quotesErrorConverter: OnrampQuotesErrorConverter,
|
||||
): OnrampRepository {
|
||||
return DefaultOnrampRepository(
|
||||
onrampApi = onrampApi,
|
||||
expressApi = expressApi,
|
||||
dispatchers = dispatchers,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
paymentMethodsStore = paymentMethodsStore,
|
||||
pairsStore = pairsStore,
|
||||
quotesStore = quotesStore,
|
||||
quotesErrorConverter = quotesErrorConverter,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -59,4 +68,11 @@ internal object OnrampDataModule {
|
|||
fun provideOnrampErrorResolver(): OnrampErrorResolver {
|
||||
return DefaultOnrampErrorResolver()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
internal fun provideErrorsConverter(@NetworkMoshi moshi: Moshi): OnrampQuotesErrorConverter {
|
||||
val jsonAdapter = moshi.adapter(ExpressErrorResponse::class.java)
|
||||
return OnrampQuotesErrorConverter(jsonAdapter)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue