Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-28 18:35:01 +05:00
parent 38276b01ea
commit 9378e4fa31
63 changed files with 1434 additions and 75 deletions

View file

@ -20,6 +20,7 @@ dependencies {
/** Domain modules */
implementation(projects.domain.onramp)
implementation(projects.domain.legacy)
// region DI

View file

@ -1,5 +1,7 @@
package com.tangem.data.onramp
import com.squareup.moshi.Moshi
import com.tangem.blockchain.extensions.toBigDecimalOrDefault
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.onramp.converters.CountryConverter
import com.tangem.data.onramp.converters.CurrencyConverter
@ -12,13 +14,16 @@ 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.express.models.response.ExchangeProviderType
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.OnrampDataJson
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.crypto.DataSignatureVerifier
import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore
import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore
import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore
@ -28,20 +33,26 @@ 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.*
import com.tangem.domain.onramp.model.cache.OnrampTransaction
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.UserWalletId
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.flow.mapNotNull
import kotlinx.coroutines.withContext
import org.joda.time.DateTime
import timber.log.Timber
import java.math.BigDecimal
import java.util.UUID
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass", "TooManyFunctions")
internal class DefaultOnrampRepository(
private val onrampApi: OnrampApi,
private val expressApi: TangemExpressApi,
@ -51,12 +62,16 @@ internal class DefaultOnrampRepository(
private val pairsStore: OnrampPairsStore,
private val quotesStore: OnrampQuotesStore,
private val quotesErrorConverter: OnrampQuotesErrorConverter,
private val walletManagersFacade: WalletManagersFacade,
private val dataSignatureVerifier: DataSignatureVerifier,
moshi: Moshi,
) : OnrampRepository {
private val currencyConverter = CurrencyConverter()
private val countryConverter = CountryConverter(currencyConverter)
private val statusConverter = StatusConverter()
private val paymentMethodsConverter = PaymentMethodConverter()
private val onrampDataAdapter = moshi.adapter(OnrampDataJson::class.java)
override suspend fun getCurrencies(): List<OnrampCurrency> = withContext(dispatchers.io) {
onrampApi.getCurrencies()
@ -194,14 +209,19 @@ internal class DefaultOnrampRepository(
providerId = provider.id,
).bind()
OnrampQuote.Data(
fromAmount = convertToBigDecimal(response.fromAmount),
toAmount = convertToBigDecimal(response.toAmount),
minFromAmount = convertToBigDecimal(response.minFromAmount),
maxFromAmount = convertToBigDecimal(response.maxFromAmount),
fromAmount = OnrampAmount(
value = response.fromAmount
.toBigDecimalOrDefault()
.movePointLeft(currency.precision),
decimals = currency.precision,
symbol = amount.currencySymbol,
),
toAmount = convertToAmount(response.toAmount, cryptoCurrency),
minFromAmount = convertToAmount(response.minFromAmount, cryptoCurrency),
maxFromAmount = convertToAmount(response.maxFromAmount, cryptoCurrency),
paymentMethod = paymentMethod,
provider = provider,
)
null
},
onError = { error ->
if (error is ApiResponseError.HttpException) {
@ -230,6 +250,69 @@ internal class DefaultOnrampRepository(
return quotesStore.get(QUOTES_KEY)
}
override suspend fun getQuotesSync(): List<OnrampQuote>? {
return quotesStore.getSyncOrNull(QUOTES_KEY)
}
override suspend fun getOnrampData(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
quote: OnrampProviderWithQuote.Data,
): OnrampTransaction = withContext(dispatchers.io) {
try {
val address = requireNotNull(
value = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network),
lazyMessage = { "Address must not be null" },
)
val fromAmountString = quote.fromAmount.value.movePointRight(quote.fromAmount.decimals).toString()
val currency = requireNotNull(getDefaultCurrencySync()) { "Default currency must not be null" }
val country = requireNotNull(getDefaultCountrySync()) { "Default country must not be null" }
val data = safeApiCall(
call = {
onrampApi.getData(
fromCurrencyCode = currency.code,
toContractAddress = cryptoCurrency.getContractAddress(),
toNetwork = cryptoCurrency.network.backendId,
paymentMethod = quote.paymentMethod.id,
countryCode = country.code,
fromAmount = fromAmountString,
toDecimals = cryptoCurrency.decimals,
providerId = quote.provider.id,
toAddress = address,
redirectUrl = "tangem://redirect?action=dismissBrowser",
language = null,
theme = null,
requestId = UUID.randomUUID().toString(),
).bind()
},
onError = { e ->
Timber.e(e)
throw e
},
)
if (dataSignatureVerifier.verifySignature(data.signature, data.dataJson)) {
val dataJson = requireNotNull(onrampDataAdapter.fromJson(data.dataJson)) {
"Can not parse dataJson. ${data.dataJson}"
}
createOnrampTransaction(
txId = data.txId,
onrampDataJson = dataJson,
userWalletId = userWalletId,
currency = currency,
provider = quote.provider,
cryptoCurrency = cryptoCurrency,
fromAmount = quote.fromAmount.value,
toAmount = quote.toAmount.value,
)
} else {
error("Express signature invalid")
}
} catch (e: Exception) {
Timber.e(e)
throw e
}
}
override suspend fun getPaymentMethods(): List<OnrampPaymentMethod> {
val paymentMethods = requireNotNull(paymentMethodsStore.getSyncOrNull(PAYMENT_METHODS_KEY)) {
"Onramp payment methods is absent in storage"
@ -237,6 +320,17 @@ internal class DefaultOnrampRepository(
return paymentMethodsConverter.convertList(paymentMethods)
}
override suspend fun saveSelectedPaymentMethod(paymentMethod: OnrampPaymentMethod) {
val dto = paymentMethodsConverter.convertBack(paymentMethod)
paymentMethodsStore.store(SELECTED_PAYMENT_METHOD_KEY, listOf(dto))
}
override fun getSelectedPaymentMethod(): Flow<OnrampPaymentMethod> {
return paymentMethodsStore
.get(SELECTED_PAYMENT_METHOD_KEY)
.mapNotNull { paymentMethods -> paymentMethods.firstOrNull()?.let(paymentMethodsConverter::convert) }
}
override suspend fun clearCache() = withContext(NonCancellable) {
paymentMethodsStore.clear()
pairsStore.clear()
@ -264,18 +358,53 @@ internal class DefaultOnrampRepository(
pairsStore.store(PAIRS_KEY, onrampPairs)
}
private fun createOnrampTransaction(
txId: String,
onrampDataJson: OnrampDataJson,
userWalletId: UserWalletId,
currency: OnrampCurrency,
provider: OnrampProvider,
cryptoCurrency: CryptoCurrency,
fromAmount: BigDecimal,
toAmount: BigDecimal,
): OnrampTransaction {
return OnrampTransaction(
txId = txId,
userWalletId = userWalletId,
fromAmount = fromAmount,
fromCurrency = currency,
toAmount = toAmount,
toCurrencyId = cryptoCurrency.id.value,
status = OnrampStatus.Status.Expired,
externalTxUrl = onrampDataJson.externalTxUrl,
externalTxId = onrampDataJson.externalTxId,
timestamp = DateTime.now().millis,
providerName = provider.info.name,
providerImageUrl = provider.info.imageLarge,
providerType = ExchangeProviderType.ONRAMP.name,
redirectUrl = onrampDataJson.widgetUrl,
)
}
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 convertToAmount(value: String, cryptoCurrency: CryptoCurrency): OnrampAmount {
val cryptoValue = requireNotNull(value.toBigDecimalOrNull()) { "Can not parse $value to BigDecimal" }
return OnrampAmount(
value = cryptoValue.movePointLeft(cryptoCurrency.decimals),
symbol = cryptoCurrency.symbol,
decimals = cryptoCurrency.decimals,
)
}
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 SELECTED_PAYMENT_METHOD_KEY = "onramp_selected_payment_method"
const val PAIRS_KEY = "onramp_pairs"
const val QUOTES_KEY = "onramp_quotes"
}

View file

@ -2,12 +2,18 @@ package com.tangem.data.onramp.converters
import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.utils.converter.Converter
import com.tangem.utils.converter.TwoWayConverter
internal class PaymentMethodConverter : Converter<PaymentMethodDTO, OnrampPaymentMethod> {
internal class PaymentMethodConverter : TwoWayConverter<PaymentMethodDTO, OnrampPaymentMethod> {
override fun convert(value: PaymentMethodDTO): OnrampPaymentMethod = OnrampPaymentMethod(
id = value.id,
name = value.name,
imageUrl = value.image,
)
override fun convertBack(value: OnrampPaymentMethod): PaymentMethodDTO = PaymentMethodDTO(
id = value.id,
name = value.name,
image = value.imageUrl,
)
}

View file

@ -25,7 +25,7 @@ internal class TransactionConverter : TwoWayConverter<OnrampTransactionDTO, Onra
providerName = value.providerName,
providerImageUrl = value.providerImageUrl,
providerType = value.providerType,
redirectUrl = "", // not necessary field
)
}

View file

@ -3,9 +3,8 @@ 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.OnrampAmount
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(
@ -40,7 +39,6 @@ internal class OnrampQuotesErrorConverter(
amountWithOffset = minAmount,
decimals = decimals,
symbol = input.amount.currencySymbol,
type = input.amount.type,
),
)
}
@ -59,24 +57,17 @@ internal class OnrampQuotesErrorConverter(
amountWithOffset = maxAmount,
decimals = decimals,
symbol = input.amount.currencySymbol,
type = input.amount.type,
),
)
}
private fun createFromAmountWithOffset(
amountWithOffset: String,
decimals: Int,
symbol: String,
type: AmountType,
): Amount {
return Amount(
private fun createFromAmountWithOffset(amountWithOffset: String, decimals: Int, symbol: String): OnrampAmount {
return OnrampAmount(
value = requireNotNull(
amountWithOffset.toBigDecimalOrNull()?.movePointLeft(decimals),
) { "wrong amount format, use only digits" },
decimals = decimals,
currencySymbol = symbol,
type = type,
symbol = symbol,
)
}
}

View file

@ -8,6 +8,7 @@ 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.crypto.DataSignatureVerifier
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore
import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore
@ -16,6 +17,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -38,6 +40,9 @@ internal object OnrampDataModule {
pairsStore: OnrampPairsStore,
quotesStore: OnrampQuotesStore,
quotesErrorConverter: OnrampQuotesErrorConverter,
walletManagersFacade: WalletManagersFacade,
dataSignatureVerifier: DataSignatureVerifier,
@NetworkMoshi moshi: Moshi,
): OnrampRepository {
return DefaultOnrampRepository(
onrampApi = onrampApi,
@ -48,6 +53,9 @@ internal object OnrampDataModule {
pairsStore = pairsStore,
quotesStore = quotesStore,
quotesErrorConverter = quotesErrorConverter,
walletManagersFacade = walletManagersFacade,
dataSignatureVerifier = dataSignatureVerifier,
moshi = moshi,
)
}