Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-23 13:28:11 +05:00
parent e87d946e40
commit a3a18854c8
48 changed files with 544 additions and 328 deletions

View file

@ -24,6 +24,7 @@ import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO
import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO
import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO
import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.exchangeservice.swap.ExpressUtils
import com.tangem.datasource.local.onramp.countries.OnrampCountriesStore
import com.tangem.datasource.local.onramp.currencies.OnrampCurrenciesStore
import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore
@ -43,6 +44,7 @@ import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.tokens.model.Amount
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.NonCancellable
@ -82,10 +84,16 @@ internal class DefaultOnrampRepository(
override fun getCurrencies(): Flow<List<OnrampCurrency>> = currenciesStore.get(CURRENCIES_KEY)
override suspend fun fetchCurrencies() = withContext(dispatchers.io) {
override suspend fun fetchCurrencies(userWallet: UserWallet) = withContext(dispatchers.io) {
if (!currenciesStore.getSyncOrNull(CURRENCIES_KEY).isNullOrEmpty()) return@withContext
val result = onrampApi.getCurrencies()
val result = onrampApi.getCurrencies(
userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode(
userWallet = userWallet,
appPreferencesStore = appPreferencesStore,
),
)
.getOrThrow()
.map(currencyConverter::convert)
@ -98,10 +106,16 @@ internal class DefaultOnrampRepository(
return countriesStore.getSyncOrNull(COUNTRIES_KEY)
}
override suspend fun fetchCountries(): List<OnrampCountry> = withContext(dispatchers.io) {
override suspend fun fetchCountries(userWallet: UserWallet): List<OnrampCountry> = withContext(dispatchers.io) {
if (!countriesStore.getSyncOrNull(COUNTRIES_KEY).isNullOrEmpty()) return@withContext emptyList()
val result = onrampApi.getCountries()
val result = onrampApi.getCountries(
userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode(
userWallet = userWallet,
appPreferencesStore = appPreferencesStore,
),
)
.getOrThrow()
.map(countryConverter::convert)
@ -110,14 +124,27 @@ internal class DefaultOnrampRepository(
result
}
override suspend fun getCountryByIp(): OnrampCountry = withContext(dispatchers.io) {
onrampApi.getCountryByIp()
override suspend fun getCountryByIp(userWallet: UserWallet): OnrampCountry = withContext(dispatchers.io) {
onrampApi.getCountryByIp(
userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode(
userWallet = userWallet,
appPreferencesStore = appPreferencesStore,
),
)
.getOrThrow()
.let(countryConverter::convert)
}
override suspend fun getStatus(txId: String): OnrampStatus = withContext(dispatchers.io) {
onrampApi.getStatus(txId)
override suspend fun getStatus(userWallet: UserWallet, txId: String): OnrampStatus = withContext(dispatchers.io) {
onrampApi.getStatus(
userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode(
userWallet = userWallet,
appPreferencesStore = appPreferencesStore,
),
txId = txId,
)
.getOrThrow()
.let(statusConverter::convert)
}
@ -161,11 +188,19 @@ internal class DefaultOnrampRepository(
.map { it?.let(countryConverter::convert) }
}
override suspend fun fetchPaymentMethodsIfAbsent() = withContext(dispatchers.io) {
override suspend fun fetchPaymentMethodsIfAbsent(userWallet: UserWallet) = withContext(dispatchers.io) {
if (paymentMethodsStore.contains(PAYMENT_METHODS_KEY)) return@withContext
val response = safeApiCall(
call = { onrampApi.getPaymentMethods().bind() },
call = {
onrampApi.getPaymentMethods(
userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode(
userWallet = userWallet,
appPreferencesStore = appPreferencesStore,
),
).bind()
},
onError = {
Timber.w(it, "Unable to fetch onramp payment methods")
throw it
@ -174,100 +209,123 @@ internal class DefaultOnrampRepository(
paymentMethodsStore.store(PAYMENT_METHODS_KEY, response.removeApplePay())
}
override suspend fun fetchPairs(currency: OnrampCurrency, country: OnrampCountry, cryptoCurrency: CryptoCurrency) =
withContext(dispatchers.io) {
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,
),
override suspend fun fetchPairs(
userWallet: UserWallet,
currency: OnrampCurrency,
country: OnrampCountry,
cryptoCurrency: CryptoCurrency,
) = withContext(dispatchers.io) {
val onrampPairs = async {
safeApiCall(
call = {
onrampApi.getPairs(
userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode(
userWallet = userWallet,
appPreferencesStore = appPreferencesStore,
),
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
},
)
}
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())
),
).bind()
},
onError = {
Timber.w(it, "Unable to fetch onramp pairs")
throw it
},
)
}
val providers = async {
safeApiCall(
call = {
expressApi.getProviders(
userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode(
userWallet = userWallet,
appPreferencesStore = appPreferencesStore,
),
).bind()
},
onError = {
Timber.w(it, "Unable to fetch express providers")
throw it
},
)
}
storeOnrampPairs(pairs = onrampPairs.await(), providers = providers.await())
}
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 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 fromOnrampAmount = OnrampAmount(
value = fromAmount
.toBigDecimalOrDefault()
.movePointLeft(currency.precision),
decimals = currency.precision,
symbol = amount.currencySymbol,
)
val quotes: List<OnrampQuote> = pairs.flatMap { pair ->
pair.providers.flatMap { provider ->
provider.paymentMethods.map { paymentMethod ->
async {
safeApiCall(
call = {
val response = onrampApi.getQuote(
fromCurrencyCode = currency.code,
fromPrecision = currency.precision,
toContractAddress = cryptoCurrency.getContractAddress(),
toNetwork = cryptoCurrency.network.backendId,
paymentMethod = paymentMethod.id,
countryCode = country.code,
fromAmount = fromAmount,
toDecimals = cryptoCurrency.decimals,
providerId = provider.id,
).bind()
OnrampQuote.Data(
fromAmount = fromOnrampAmount,
toAmount = convertToAmount(response.toAmount, cryptoCurrency),
minFromAmount = convertToAmount(response.minFromAmount, cryptoCurrency),
maxFromAmount = convertToAmount(response.maxFromAmount, cryptoCurrency),
paymentMethod = paymentMethod,
provider = provider,
countryCode = response.countryCode,
)
},
onError = { error ->
convertQuoteError(
error = error,
paymentMethod = paymentMethod,
provider = provider,
fromOnrampAmount = fromOnrampAmount,
countryCode = country.code,
)
},
)
override suspend fun fetchQuotes(userWallet: UserWallet, 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 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 fromOnrampAmount = OnrampAmount(
value = fromAmount
.toBigDecimalOrDefault()
.movePointLeft(currency.precision),
decimals = currency.precision,
symbol = amount.currencySymbol,
)
val quotes: List<OnrampQuote> = pairs.flatMap { pair ->
pair.providers.flatMap { provider ->
provider.paymentMethods.map { paymentMethod ->
async {
safeApiCall(
call = {
val response = onrampApi.getQuote(
fromCurrencyCode = currency.code,
fromPrecision = currency.precision,
toContractAddress = cryptoCurrency.getContractAddress(),
toNetwork = cryptoCurrency.network.backendId,
paymentMethod = paymentMethod.id,
countryCode = country.code,
fromAmount = fromAmount,
toDecimals = cryptoCurrency.decimals,
providerId = provider.id,
userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode(
userWallet = userWallet,
appPreferencesStore = appPreferencesStore,
),
).bind()
OnrampQuote.Data(
fromAmount = fromOnrampAmount,
toAmount = convertToAmount(response.toAmount, cryptoCurrency),
minFromAmount = convertToAmount(response.minFromAmount, cryptoCurrency),
maxFromAmount = convertToAmount(response.maxFromAmount, cryptoCurrency),
paymentMethod = paymentMethod,
provider = provider,
countryCode = response.countryCode,
)
},
onError = { error ->
convertQuoteError(
error = error,
paymentMethod = paymentMethod,
provider = provider,
fromOnrampAmount = fromOnrampAmount,
countryCode = country.code,
)
},
)
}
}
}
}
}.awaitAll().filterNotNull()
quotesStore.store(QUOTES_KEY, quotes)
}
}.awaitAll().filterNotNull()
quotesStore.store(QUOTES_KEY, quotes)
}
override fun getQuotes(): Flow<List<OnrampQuote>> {
return quotesStore.get(QUOTES_KEY)
@ -278,14 +336,14 @@ internal class DefaultOnrampRepository(
}
override suspend fun getOnrampData(
userWalletId: UserWalletId,
userWallet: UserWallet,
cryptoCurrency: CryptoCurrency,
quote: OnrampProviderWithQuote.Data,
isDarkTheme: Boolean,
): OnrampTransaction = withContext(dispatchers.io) {
try {
val address = requireNotNull(
value = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network),
value = walletManagersFacade.getDefaultAddress(userWallet.walletId, cryptoCurrency.network),
lazyMessage = { "Address must not be null" },
)
val fromAmountString = quote.fromAmount.value.movePointRight(quote.fromAmount.decimals).toString()
@ -309,6 +367,11 @@ internal class DefaultOnrampRepository(
language = null,
theme = getTheme(isDarkTheme),
requestId = requestId,
userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode(
userWallet = userWallet,
appPreferencesStore = appPreferencesStore,
),
).bind()
},
onError = { e ->
@ -326,7 +389,7 @@ internal class DefaultOnrampRepository(
txId = data.txId,
quote = quote,
onrampDataJson = dataJson,
userWalletId = userWalletId,
userWalletId = userWallet.walletId,
currency = currency,
cryptoCurrency = cryptoCurrency,
residency = country.name,

View file

@ -618,7 +618,7 @@ internal class DefaultCurrenciesRepository(
}
coroutineScope {
launch { expressServiceLoader.update(userWalletId, tokens) }
launch { expressServiceLoader.update(getUserWallet(userWalletId), tokens) }
}
}
@ -639,7 +639,7 @@ internal class DefaultCurrenciesRepository(
skipCache = refresh,
block = {
coroutineScope {
launch { expressServiceLoader.update(userWalletId, tokens) }
launch { expressServiceLoader.update(getUserWallet(userWalletId), tokens) }
}
},
)
@ -677,13 +677,13 @@ internal class DefaultCurrenciesRepository(
isSortedByBalance = false,
)
private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet {
private fun getUserWallet(userWalletId: UserWalletId): UserWallet {
return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"Unable to find a user wallet with provided ID: $userWalletId"
}
}
private suspend fun ensureIsCorrectUserWallet(userWalletId: UserWalletId, isMultiCurrencyWalletExpected: Boolean) {
private fun ensureIsCorrectUserWallet(userWalletId: UserWalletId, isMultiCurrencyWalletExpected: Boolean) {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected)