Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-07 20:17:00 +04:00
parent 0c56df0075
commit 535bb9b8c3
44 changed files with 128 additions and 1038 deletions

View file

@ -1,31 +0,0 @@
package com.tangem.tap.network.exchangeServices
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.features.demo.isDemoCard
/**
[REDACTED_AUTHOR]
*/
class CardExchangeRules(
val cardProvider: () -> CardDTO?,
) : ExchangeRules {
override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean {
val card = scanResponse.card
return when {
card.isDemoCard() -> true
card.isStart2Coin -> false
else -> true
}
}
override fun availableForSell(currency: Currency): Boolean {
val card = cardProvider() ?: return false
return !card.isStart2Coin
}
}

View file

@ -1,148 +0,0 @@
package com.tangem.tap.network.exchangeServices
import com.tangem.Message
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TangemSigner
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
class CurrencyExchangeManager(
private val buyService: ExchangeService,
private val sellService: ExchangeService,
private val primaryRules: ExchangeRules,
) : ExchangeService {
override val initializationStatus: StateFlow<ExchangeServiceInitializationStatus>
get() = _initializationStatus
private val _initializationStatus: MutableStateFlow<ExchangeServiceInitializationStatus> =
MutableStateFlow(value = lceLoading())
override suspend fun update() {
_initializationStatus.value = lceLoading()
if (!store.inject(DaggerGraphState::onrampFeatureToggles).isFeatureEnabled) {
buyService.update()
}
sellService.update()
_initializationStatus.value = lceContent()
}
override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean {
return primaryRules.availableForBuy(scanResponse, currency) &&
buyService.availableForBuy(scanResponse, currency)
}
override fun availableForSell(currency: Currency): Boolean {
return primaryRules.availableForSell(currency) && sellService.availableForSell(currency)
}
override fun getUrl(
action: Action,
cryptoCurrency: CryptoCurrency,
fiatCurrencyName: String,
walletAddress: String,
isDarkTheme: Boolean,
): String? {
val blockchain = cryptoCurrency.network.toBlockchain()
if (blockchain.isTestnet()) return blockchain.getTestnetTopUpUrl()
val urlBuilder = getExchangeUrlBuilder(action)
return urlBuilder.getUrl(
action,
cryptoCurrency,
fiatCurrencyName,
walletAddress,
isDarkTheme,
)
}
override fun getSellCryptoReceiptUrl(action: Action, transactionId: String): String? {
val urlBuilder = getExchangeUrlBuilder(action)
return urlBuilder.getSellCryptoReceiptUrl(action, transactionId)
}
private fun getExchangeUrlBuilder(action: Action): ExchangeUrlBuilder {
return when (action) {
Action.Buy -> buyService
Action.Sell -> sellService
}
}
enum class Action { Buy, Sell }
companion object {
fun dummy(): CurrencyExchangeManager = CurrencyExchangeManager(
buyService = ExchangeService.dummy(),
sellService = ExchangeService.dummy(),
primaryRules = ExchangeRules.dummy(),
)
}
}
@Deprecated("Useless")
suspend fun buyErc20TestnetTokens(card: CardDTO, walletManager: EthereumWalletManager, destinationAddress: String) {
walletManager.safeUpdate(card.isDemoCard())
val amountToSend = Amount(walletManager.wallet.blockchain)
val feeResult = walletManager.getFee(amountToSend, destinationAddress) as? Result.Success ?: return
val fee = when (val feeForTx = feeResult.data) {
is TransactionFee.Choosable -> feeForTx.minimum
is TransactionFee.Single -> feeForTx.normal
}
val coinValue = walletManager.wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO
if (coinValue < fee.amount.value) return
val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins
val signer = TangemSigner(
cardId = card.cardId.takeIf { isCardNotBackedUp },
tangemSdk = store.inject(DaggerGraphState::cardSdkConfigRepository).sdk,
initialMessage = Message(),
twinKey = null,
) { signResponse ->
store.dispatch(
GlobalAction.UpdateWalletSignedHashes(
walletSignedHashes = signResponse.totalSignedHashes,
walletPublicKey = walletManager.wallet.publicKey.seedKey,
remainingSignatures = signResponse.remainingSignatures,
),
)
store.dispatch(action = GlobalAction.IsSignWithRing(signResponse.isRing))
}
walletManager.send(
transactionData = walletManager.createTransaction(
amount = amountToSend,
fee = fee,
destination = destinationAddress,
),
signer = signer,
)
}

View file

@ -20,7 +20,6 @@ import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.onramp.OnrampFeatureToggles
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
@ -30,14 +29,11 @@ import kotlinx.coroutines.flow.firstOrNull
@Suppress("LongParameterList")
internal class DefaultRampManager(
private val exchangeService: ExchangeService?,
private val buyService: Provider<ExchangeService>,
private val sellService: Provider<ExchangeService>,
private val expressServiceLoader: ExpressServiceLoader,
private val currenciesRepository: CurrenciesRepository,
private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase,
private val dispatchers: CoroutineDispatcherProvider,
private val onrampFeatureToggles: OnrampFeatureToggles,
excludedBlockchains: ExcludedBlockchains,
) : RampStateManager {
@ -48,9 +44,9 @@ internal class DefaultRampManager(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): ScenarioUnavailabilityReason {
val availabilityState = runCatching {
getOnrampAvailableState(scanResponse, userWalletId, cryptoCurrency)
}.getOrNull() ?: ExpressAvailabilityState.Error
val availabilityState = runCatching { getOnrampAvailableState(userWalletId, cryptoCurrency) }
.getOrNull()
?: ExpressAvailabilityState.Error
return availabilityState.toReason(cryptoCurrency.name)
}
@ -64,7 +60,7 @@ internal class DefaultRampManager(
block = {
val serviceCurrency = cryptoCurrencyConverter.convertBack(status.currency)
exchangeService?.availableForSell(currency = serviceCurrency) ?: false
sellService().availableForSell(currency = serviceCurrency)
},
catch = { raise(ScenarioUnavailabilityReason.NotSupportedBySellService(status.currency.name)) },
)
@ -101,16 +97,6 @@ internal class DefaultRampManager(
return availabilityState.toReason(cryptoCurrency.name)
}
override fun getBuyInitializationStatus(): Flow<ExchangeServiceInitializationStatus> {
return buyService.invoke().initializationStatus
}
override suspend fun fetchBuyServiceData() {
runCatching(dispatchers.io) {
buyService.invoke().update()
}
}
override fun getSellInitializationStatus(): Flow<ExchangeServiceInitializationStatus> {
return sellService.invoke().initializationStatus
}
@ -143,31 +129,20 @@ internal class DefaultRampManager(
}
private suspend fun getOnrampAvailableState(
scanResponse: ScanResponse,
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): ExpressAvailabilityState {
return when {
onrampFeatureToggles.isFeatureEnabled -> {
val asset = expressServiceLoader.getInitializationStatus(userWalletId).firstOrNull()
?: return ExpressAvailabilityState.Loading
return when (asset) {
is Lce.Error -> ExpressAvailabilityState.Error
is Lce.Loading -> ExpressAvailabilityState.Loading
is Lce.Content -> {
val foundAsset = asset.getOrNull()?.find { cryptoCurrency.findAssetPredicate(it) }
foundAsset?.onrampAvailable?.toOnrampAvailabilityState()
?: ExpressAvailabilityState.AssetNotFound
}
}
val asset = expressServiceLoader.getInitializationStatus(userWalletId).firstOrNull()
?: return ExpressAvailabilityState.Loading
return when (asset) {
is Lce.Error -> ExpressAvailabilityState.Error
is Lce.Loading -> ExpressAvailabilityState.Loading
is Lce.Content -> {
val foundAsset = asset.getOrNull()?.find { cryptoCurrency.findAssetPredicate(it) }
foundAsset?.onrampAvailable?.toOnrampAvailabilityState()
?: ExpressAvailabilityState.AssetNotFound
}
exchangeService != null -> {
exchangeService.availableForBuy(
scanResponse = scanResponse,
currency = cryptoCurrencyConverter.convertBack(cryptoCurrency),
).toOnrampAvailabilityState()
}
else -> ExpressAvailabilityState.Error
}
}

View file

@ -1,75 +1,26 @@
package com.tangem.tap.network.exchangeServices
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.model.Currency
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
typealias ExchangeServiceInitializationStatus = Lce<Throwable, Any>
interface Exchanger {
fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean
fun availableForSell(currency: Currency): Boolean
}
interface ExchangeService : Exchanger, ExchangeUrlBuilder {
interface ExchangeService {
val initializationStatus: StateFlow<ExchangeServiceInitializationStatus>
suspend fun update()
companion object {
fun dummy(): ExchangeService = object : ExchangeService {
fun availableForSell(currency: Currency): Boolean
override val initializationStatus: StateFlow<ExchangeServiceInitializationStatus> =
MutableStateFlow(value = lceLoading())
override suspend fun update() {}
override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean = false
override fun availableForSell(currency: Currency): Boolean = false
override fun getUrl(
action: CurrencyExchangeManager.Action,
cryptoCurrency: CryptoCurrency,
fiatCurrencyName: String,
walletAddress: String,
isDarkTheme: Boolean,
): String? = null
override fun getSellCryptoReceiptUrl(
action: CurrencyExchangeManager.Action,
transactionId: String,
): String? = null
}
}
}
interface ExchangeRules : Exchanger {
companion object {
fun dummy(): ExchangeRules = object : ExchangeRules {
override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean = false
override fun availableForSell(currency: Currency): Boolean = false
}
}
}
interface ExchangeUrlBuilder {
@Suppress("LongParameterList")
fun getUrl(
action: CurrencyExchangeManager.Action,
cryptoCurrency: CryptoCurrency,
fiatCurrencyName: String,
walletAddress: String,
isDarkTheme: Boolean,
): String?
fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String?
companion object {
const val SCHEME = "https"
const val SUCCESS_URL = "https://tangem.com/success"
}
fun getSellCryptoReceiptUrl(transactionId: String): String?
}

View file

@ -1,42 +0,0 @@
package com.tangem.tap.network.exchangeServices.mercuryo
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import retrofit2.http.GET
import retrofit2.http.Path
interface MercuryoApi {
@GET("{apiVersion}/lib/currencies")
suspend fun currencies(@Path("apiVersion") apiVersion: String): MercuryoCurrenciesResponse
}
@JsonClass(generateAdapter = true)
data class MercuryoCurrenciesResponse(
@Json(name = "status") val status: Int,
@Json(name = "data") val data: Data,
) {
@JsonClass(generateAdapter = true)
data class Data(
@Json(name = "fiat") val fiat: List<String>,
@Json(name = "crypto") val crypto: List<String>,
@Json(name = "config") val config: Config,
)
@JsonClass(generateAdapter = true)
data class Config(
@Json(name = "crypto_currencies")
val cryptoCurrencies: List<MercuryoCryptoCurrency>,
)
@JsonClass(generateAdapter = true)
data class MercuryoCryptoCurrency(
@Json(name = "currency")
val currencySymbol: String,
@Json(name = "network")
val network: String,
@Json(name = "contract")
val contractAddress: String,
)
}

View file

@ -1,32 +0,0 @@
package com.tangem.tap.network.exchangeServices.mercuryo
import com.tangem.datasource.api.common.createRetrofitInstance
/**
[REDACTED_AUTHOR]
*/
data class MercuryoEnvironment(
val baseUrl: String,
val apiVersion: String,
val widgetId: String,
val secret: String,
val mercuryoApi: MercuryoApi,
) {
companion object {
private const val BASE_URL = "https://api.mercuryo.io/"
private const val API_VERSION = "v1.6"
fun prod(widgetId: String, secret: String, apiVersion: String = API_VERSION): MercuryoEnvironment {
return MercuryoEnvironment(
baseUrl = BASE_URL,
apiVersion = apiVersion,
widgetId = widgetId,
secret = secret,
mercuryoApi = createRetrofitInstance(
baseUrl = BASE_URL,
logEnabled = false,
).create(MercuryoApi::class.java),
)
}
}
}

View file

@ -1,116 +0,0 @@
package com.tangem.tap.network.exchangeServices.mercuryo
import android.net.Uri
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.common.extensions.calculateSha512
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.data.onramp.legacy.mercuryoNetwork
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.ExchangeServiceInitializationStatus
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber
import java.util.concurrent.CopyOnWriteArrayList
/**
[REDACTED_AUTHOR]
*/
internal class MercuryoService(private val environment: MercuryoEnvironment) : ExchangeService {
override val initializationStatus: StateFlow<ExchangeServiceInitializationStatus>
get() = _initializationStatus
private val _initializationStatus: MutableStateFlow<ExchangeServiceInitializationStatus> =
MutableStateFlow(value = lceLoading())
private val api: MercuryoApi = environment.mercuryoApi
private val availableMercuryoCurrencies = CopyOnWriteArrayList<MercuryoCurrenciesResponse.MercuryoCryptoCurrency>()
override suspend fun update() {
Timber.i("Start updating")
_initializationStatus.value = lceLoading()
val result = performRequest { api.currencies(environment.apiVersion) }
when {
result is Result.Success && result.data.status == RESPONSE_SUCCESS_STATUS_CODE -> {
handleSuccessfullyUpdatedData(data = result.data.data)
}
result is Result.Failure -> {
availableMercuryoCurrencies.clear()
Timber.e("Failed to load currencies", result.error)
_initializationStatus.value = result.error.lceError()
}
}
}
override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean {
val mercuryoNetwork = currency.blockchain.mercuryoNetwork
val contractAddress = (currency as? Currency.Token)?.token?.contractAddress ?: ""
val availableCurrency = availableMercuryoCurrencies.firstOrNull {
it.currencySymbol == currency.currencySymbol &&
it.network == mercuryoNetwork &&
it.contractAddress.equals(contractAddress, ignoreCase = true)
}
return availableCurrency != null
}
override fun availableForSell(currency: Currency): Boolean = false
override fun getUrl(
action: CurrencyExchangeManager.Action,
cryptoCurrency: CryptoCurrency,
fiatCurrencyName: String,
walletAddress: String,
isDarkTheme: Boolean,
): String {
if (action == CurrencyExchangeManager.Action.Sell) throw UnsupportedOperationException()
val blockchain = cryptoCurrency.network.toBlockchain()
val builder = Uri.Builder()
.scheme(ExchangeUrlBuilder.SCHEME)
.authority("exchange.mercuryo.io")
.appendQueryParameter("widget_id", environment.widgetId)
.appendQueryParameter("type", action.name.lowercase())
.appendQueryParameter("currency", cryptoCurrency.symbol)
.appendQueryParameter("address", walletAddress)
.appendQueryParameter("signature", signature(walletAddress))
.appendQueryParameter("fix_currency", "true")
.appendQueryParameter("redirect_url", ExchangeUrlBuilder.SUCCESS_URL)
if (isDarkTheme) builder.appendQueryParameter("theme", "1inch")
blockchain.mercuryoNetwork?.let {
builder.appendQueryParameter("network", it)
}
return builder.build().toString()
}
override fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String? = null
private fun handleSuccessfullyUpdatedData(data: MercuryoCurrenciesResponse.Data) {
availableMercuryoCurrencies.clear()
availableMercuryoCurrencies.addAll(data.config.cryptoCurrencies)
Timber.i("Successfully updated")
_initializationStatus.value = lceContent()
}
private fun signature(address: String) = (address + environment.secret).calculateSha512().toHexString().lowercase()
private companion object {
const val RESPONSE_SUCCESS_STATUS_CODE = 200
}
}

View file

@ -6,17 +6,16 @@ import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.datasource.api.common.createRetrofitInstance
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.extensions.withIOContext
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.scan.CardDTO
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.ExchangeServiceInitializationStatus
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.SCHEME
import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPayAvailableCurrency
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -28,6 +27,7 @@ class MoonPayService(
private val apiKey: String,
private val secretKey: String,
private val logEnabled: Boolean,
private val cardProvider: () -> CardDTO?,
) : ExchangeService {
override val initializationStatus: StateFlow<ExchangeServiceInitializationStatus>
@ -102,9 +102,12 @@ class MoonPayService(
}
}
override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean = false
override fun availableForSell(currency: Currency): Boolean {
val card = cardProvider() ?: return false
val checkCardExchange = !card.isStart2Coin
if (!checkCardExchange) return false
if (!isSellAllowed()) return false
val availableForSell = status?.availableForSell ?: return false
@ -125,15 +128,14 @@ class MoonPayService(
}
override fun getUrl(
action: CurrencyExchangeManager.Action,
cryptoCurrency: CryptoCurrency,
fiatCurrencyName: String,
walletAddress: String,
isDarkTheme: Boolean,
): String? {
if (action == CurrencyExchangeManager.Action.Buy) throw UnsupportedOperationException()
val blockchain = cryptoCurrency.network.toBlockchain()
if (blockchain.isTestnet()) return blockchain.getTestnetTopUpUrl()
val supportedCurrency = blockchain.moonPaySupportedCurrency ?: return null
val moonpayCurrency = status?.availableForSell?.firstOrNull {
when (cryptoCurrency) {
@ -165,7 +167,7 @@ class MoonPayService(
return uri.build().toString()
}
override fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String {
override fun getSellCryptoReceiptUrl(transactionId: String): String {
return Uri.Builder()
.scheme(SCHEME)
.authority(URL_SELL)
@ -185,8 +187,9 @@ class MoonPayService(
return status?.responseUserStatus?.isSellAllowed ?: false
}
companion object {
private companion object {
const val URL_SELL = "sell.moonpay.com"
const val SCHEME = "https"
}
}