Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-01 22:41:08 +03:00
parent ed9e188459
commit 28d15c9043
9 changed files with 142 additions and 127 deletions

View file

@ -2,7 +2,6 @@ package com.tangem.tap.di
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.exchange.RampStateManager
@ -55,7 +54,6 @@ internal object ActivityModule {
excludedBlockchains: ExcludedBlockchains,
dispatchers: CoroutineDispatcherProvider,
onrampFeatureToggles: OnrampFeatureToggles,
expressAssetsStore: ExpressAssetsStore,
): RampStateManager {
return DefaultRampManager(
exchangeService = appStateHolder.exchangeService,
@ -67,7 +65,6 @@ internal object ActivityModule {
excludedBlockchains = excludedBlockchains,
dispatchers = dispatchers,
onrampFeatureToggles = onrampFeatureToggles,
expressAssetsStore = expressAssetsStore,
)
}

View file

@ -9,9 +9,8 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.exchange.ExchangeableState
import com.tangem.domain.exchange.ExpressAvailabilityState
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase
@ -27,7 +26,6 @@ import com.tangem.utils.coroutines.runCatching
import com.tangem.utils.isNullOrZero
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withContext
@Suppress("LongParameterList")
internal class DefaultRampManager(
@ -39,7 +37,6 @@ internal class DefaultRampManager(
private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase,
private val dispatchers: CoroutineDispatcherProvider,
private val onrampFeatureToggles: OnrampFeatureToggles,
private val expressAssetsStore: ExpressAssetsStore,
excludedBlockchains: ExcludedBlockchains,
) : RampStateManager {
@ -49,19 +46,12 @@ internal class DefaultRampManager(
scanResponse: ScanResponse,
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): Boolean {
return runCatching {
when {
onrampFeatureToggles.isFeatureEnabled -> getOnrampAvailable(userWalletId, cryptoCurrency)
exchangeService != null -> exchangeService.availableForBuy(
scanResponse = scanResponse,
currency = cryptoCurrencyConverter.convertBack(cryptoCurrency),
)
else -> false
}
}
.getOrNull()
?: false
): ScenarioUnavailabilityReason {
val availabilityState = runCatching {
getOnrampAvailableState(scanResponse, userWalletId, cryptoCurrency)
}.getOrNull() ?: ExpressAvailabilityState.Error
return availabilityState.toReason(cryptoCurrency.name)
}
override suspend fun availableForSell(
@ -103,8 +93,11 @@ internal class DefaultRampManager(
override suspend fun availableForSwap(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): ExchangeableState {
return runCatching { getExchangeableState(userWalletId, cryptoCurrency) }.getOrNull() ?: ExchangeableState.Error
): ScenarioUnavailabilityReason {
val availabilityState = runCatching {
getExchangeableState(userWalletId, cryptoCurrency)
}.getOrNull() ?: ExpressAvailabilityState.Error
return availabilityState.toReason(cryptoCurrency.name)
}
override fun getBuyInitializationStatus(): Flow<ExchangeServiceInitializationStatus> {
@ -134,33 +127,76 @@ internal class DefaultRampManager(
private suspend fun getExchangeableState(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): ExchangeableState {
return withContext(dispatchers.io) {
val asset = expressServiceLoader.getInitializationStatus(userWalletId).firstOrNull()
?: return@withContext ExchangeableState.Loading
when (asset) {
is Lce.Error -> ExchangeableState.Error
is Lce.Loading -> ExchangeableState.Loading
is Lce.Content -> {
val foundAsset = asset.getOrNull()?.find { cryptoCurrency.findAssetPredicate(it) }
foundAsset?.exchangeAvailable?.toExchangeableState() ?: ExchangeableState.AssetNotFound
}
): ExpressAvailabilityState {
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?.exchangeAvailable?.toSwapAvailabilityState()
?: ExpressAvailabilityState.AssetNotFound
}
}
}
private fun Boolean.toExchangeableState(): ExchangeableState {
return if (this) {
ExchangeableState.Exchangeable
} else {
ExchangeableState.NotExchangeable
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
}
}
}
exchangeService != null -> {
exchangeService.availableForBuy(
scanResponse = scanResponse,
currency = cryptoCurrencyConverter.convertBack(cryptoCurrency),
).toOnrampAvailabilityState()
}
else -> ExpressAvailabilityState.Error
}
}
private suspend fun getOnrampAvailable(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
val asset = expressAssetsStore.getSyncOrNull(userWalletId)?.find { cryptoCurrency.findAssetPredicate(it) }
private fun ExpressAvailabilityState.toReason(currencyName: String): ScenarioUnavailabilityReason {
return when (this) {
ExpressAvailabilityState.Available -> {
ScenarioUnavailabilityReason.None
}
ExpressAvailabilityState.AssetNotFound -> ScenarioUnavailabilityReason.AssetNotFound(currencyName)
ExpressAvailabilityState.Error -> ScenarioUnavailabilityReason.ExpressUnreachable(currencyName)
ExpressAvailabilityState.Loading -> ScenarioUnavailabilityReason.ExpressLoading(currencyName)
ExpressAvailabilityState.NotOnrampable -> ScenarioUnavailabilityReason.BuyUnavailable(currencyName)
ExpressAvailabilityState.NotExchangeable -> ScenarioUnavailabilityReason.NotExchangeable(currencyName)
}
}
return asset?.onrampAvailable ?: false
private fun Boolean.toSwapAvailabilityState(): ExpressAvailabilityState {
return if (this) {
ExpressAvailabilityState.Available
} else {
ExpressAvailabilityState.NotExchangeable
}
}
private fun Boolean.toOnrampAvailabilityState(): ExpressAvailabilityState {
return if (this) {
ExpressAvailabilityState.Available
} else {
ExpressAvailabilityState.NotOnrampable
}
}
private fun CryptoCurrency.findAssetPredicate(asset: Asset): Boolean {

View file

@ -1,9 +0,0 @@
package com.tangem.domain.exchange
sealed interface ExchangeableState {
data object Exchangeable : ExchangeableState
data object NotExchangeable : ExchangeableState
data object Loading : ExchangeableState
data object Error : ExchangeableState
data object AssetNotFound : ExchangeableState
}

View file

@ -0,0 +1,10 @@
package com.tangem.domain.exchange
sealed interface ExpressAvailabilityState {
data object Available : ExpressAvailabilityState
data object NotExchangeable : ExpressAvailabilityState
data object NotOnrampable : ExpressAvailabilityState
data object Loading : ExpressAvailabilityState
data object Error : ExpressAvailabilityState
data object AssetNotFound : ExpressAvailabilityState
}

View file

@ -18,7 +18,7 @@ interface RampStateManager {
scanResponse: ScanResponse,
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): Boolean
): ScenarioUnavailabilityReason
/**
* Check if [CryptoCurrency] is available for sell
@ -31,7 +31,10 @@ interface RampStateManager {
status: CryptoCurrencyStatus,
): Either<ScenarioUnavailabilityReason, Unit>
suspend fun availableForSwap(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): ExchangeableState
suspend fun availableForSwap(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): ScenarioUnavailabilityReason
suspend fun fetchBuyServiceData()

View file

@ -1,7 +1,6 @@
package com.tangem.domain.tokens
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.exchange.ExchangeableState
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.models.StatusSource
import com.tangem.domain.promo.PromoRepository
@ -168,12 +167,11 @@ class GetCryptoCurrencyActionsUseCase(
}
// buy
if (rampManager.availableForBuy(userWallet.scanResponse, userWallet.walletId, cryptoCurrency)) {
activeList.add(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None))
val onrampActionState = getOnrampUnavailabilityReason(userWallet, cryptoCurrencyStatus)
if (onrampActionState.unavailabilityReason == ScenarioUnavailabilityReason.None) {
activeList.add(onrampActionState)
} else {
disabledList.add(
TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.BuyUnavailable(cryptoCurrency.symbol)),
)
disabledList.add(onrampActionState)
}
// region sell
@ -229,38 +227,37 @@ class GetCryptoCurrencyActionsUseCase(
cryptoCurrencyStatus: CryptoCurrencyStatus,
requirements: AssetRequirementsCondition?,
): List<TokenActionsState.ActionState> {
val actionsList = mutableListOf<TokenActionsState.ActionState>()
val activeList = mutableListOf<TokenActionsState.ActionState>()
val disabledList = mutableListOf<TokenActionsState.ActionState>()
if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) {
actionsList.add(TokenActionsState.ActionState.CopyAddress(ScenarioUnavailabilityReason.None))
activeList.add(TokenActionsState.ActionState.CopyAddress(ScenarioUnavailabilityReason.None))
}
if (rampManager.availableForBuy(userWallet.scanResponse, userWallet.walletId, cryptoCurrencyStatus.currency)) {
actionsList.add(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None))
// buy (is not depend on cache)
val onrampActionState = getOnrampUnavailabilityReason(userWallet, cryptoCurrencyStatus)
if (onrampActionState.unavailabilityReason == ScenarioUnavailabilityReason.None) {
activeList.add(onrampActionState)
} else {
actionsList.add(
TokenActionsState.ActionState.Buy(
ScenarioUnavailabilityReason.BuyUnavailable(
cryptoCurrencyName = cryptoCurrencyStatus.currency.name,
),
),
)
disabledList.add(onrampActionState)
}
actionsList.add(TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.Unreachable))
actionsList.add(
disabledList.add(TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.Unreachable))
disabledList.add(
TokenActionsState.ActionState.Swap(
unavailabilityReason = ScenarioUnavailabilityReason.Unreachable,
showBadge = false,
),
)
actionsList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.Unreachable))
disabledList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.Unreachable))
if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) {
val scenario = getReceiveScenario(requirements)
actionsList.add(TokenActionsState.ActionState.Receive(scenario))
activeList.add(TokenActionsState.ActionState.Receive(scenario))
}
actionsList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.Unreachable, null))
actionsList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None))
disabledList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.Unreachable, null))
activeList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None))
return actionsList
return activeList + disabledList
}
@Suppress("LongMethod")
@ -313,17 +310,12 @@ class GetCryptoCurrencyActionsUseCase(
disabledList.add(swapAction)
}
// buy
if (rampManager.availableForBuy(userWallet.scanResponse, userWallet.walletId, cryptoCurrency)) {
activeList.add(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None))
// buy (is not depend on cache)
val onrampActionState = getOnrampUnavailabilityReason(userWallet, cryptoCurrencyStatus)
if (onrampActionState.unavailabilityReason == ScenarioUnavailabilityReason.None) {
activeList.add(onrampActionState)
} else {
disabledList.add(
TokenActionsState.ActionState.Buy(
ScenarioUnavailabilityReason.BuyUnavailable(
cryptoCurrencyName = cryptoCurrencyStatus.currency.name,
),
),
)
disabledList.add(onrampActionState)
}
// staking
@ -415,21 +407,7 @@ class GetCryptoCurrencyActionsUseCase(
showBadge = false,
)
}
val exchangeableState = withTimeoutOrNull(REQUEST_EXCHANGE_DATA_TIMEOUT) {
rampManager.availableForSwap(userWallet.walletId, cryptoCurrency)
} ?: ExchangeableState.Loading
val currencyName = cryptoCurrency.name
val reason = when (exchangeableState) {
ExchangeableState.Exchangeable -> {
ScenarioUnavailabilityReason.None
}
ExchangeableState.AssetNotFound -> ScenarioUnavailabilityReason.AssetNotFound(currencyName)
ExchangeableState.Error -> ScenarioUnavailabilityReason.ExpressUnreachable(currencyName)
ExchangeableState.Loading -> ScenarioUnavailabilityReason.ExpressLoading(currencyName)
ExchangeableState.NotExchangeable -> ScenarioUnavailabilityReason.NotExchangeable(currencyName)
}
val reason = rampManager.availableForSwap(userWallet.walletId, cryptoCurrency)
val isShowBadge = reason == ScenarioUnavailabilityReason.None && shouldShowSwapStories
TokenActionsState.ActionState.Swap(
unavailabilityReason = reason,
@ -443,6 +421,15 @@ class GetCryptoCurrencyActionsUseCase(
}
}
private suspend fun getOnrampUnavailabilityReason(
userWallet: UserWallet,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): TokenActionsState.ActionState {
val cryptoCurrency = cryptoCurrencyStatus.currency
val reason = rampManager.availableForBuy(userWallet.scanResponse, userWallet.walletId, cryptoCurrency)
return TokenActionsState.ActionState.Buy(unavailabilityReason = reason)
}
private fun isAddressAvailable(networkAddress: NetworkAddress?): Boolean {
return networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty()
}

View file

@ -20,6 +20,7 @@ import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.models.UserWallet
@ -171,22 +172,18 @@ internal class OnboardingNoteTopUpModel @Inject constructor(
_uiState.update {
it.copy(
amountToCreateAccount = amountToCreateAccount
?.format(
{
crypto(
symbol = status.currency.symbol,
decimals = status.currency.decimals,
)
},
),
balance = amount?.format(
{
?.format {
crypto(
symbol = status.currency.symbol,
decimals = status.currency.decimals,
)
},
).orEmpty(),
balance = amount?.format {
crypto(
symbol = status.currency.symbol,
decimals = status.currency.decimals,
)
}.orEmpty(),
isTopUpDataLoading = status.value.networkAddress == null,
)
}
@ -209,7 +206,7 @@ internal class OnboardingNoteTopUpModel @Inject constructor(
)
_uiState.update {
it.copy(
availableForBuy = availableForBuy,
availableForBuy = availableForBuy == ScenarioUnavailabilityReason.None,
availableForBuyLoading = false,
)
}

View file

@ -20,6 +20,7 @@ import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.onramp.OnrampFeatureToggles
import com.tangem.features.onramp.impl.R
@ -79,13 +80,13 @@ internal class OnrampOperationModel @Inject constructor(
fun onHotTokenClick(status: CryptoCurrencyStatus) {
modelScope.launch {
val isAvailable = rampStateManager.availableForBuy(
val unavailabilityReason = rampStateManager.availableForBuy(
scanResponse = selectedUserWallet.scanResponse,
userWalletId = params.userWalletId,
cryptoCurrency = status.currency,
)
if (isAvailable) {
if (unavailabilityReason == ScenarioUnavailabilityReason.None) {
analyticsEventHandler.send(
event = MainScreenAnalyticsEvent.HotTokenClicked(currencySymbol = status.currency.symbol),
)

View file

@ -11,13 +11,13 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.utils.getOrElse
import com.tangem.domain.exchange.ExchangeableState
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
@ -230,7 +230,7 @@ internal class OnrampTokenListModel @Inject constructor(
scanResponse = scanResponse,
userWalletId = params.userWalletId,
cryptoCurrency = status.currency,
)
).isAvailable()
}
OnrampOperation.SELL -> {
rampStateManager.availableForSell(
@ -242,21 +242,14 @@ internal class OnrampTokenListModel @Inject constructor(
val isAvailable = rampStateManager.availableForSwap(
userWalletId = params.userWalletId,
cryptoCurrency = status.currency,
).isSwapAvailable() && !status.currency.isCustom
).isAvailable() && !status.currency.isCustom
isAvailable && status.value !is CryptoCurrencyStatus.NoQuote
}
}
}
private fun ExchangeableState.isSwapAvailable(): Boolean {
return when (this) {
ExchangeableState.AssetNotFound,
ExchangeableState.Error,
ExchangeableState.Loading,
ExchangeableState.NotExchangeable,
-> false
ExchangeableState.Exchangeable -> true
}
private fun ScenarioUnavailabilityReason.isAvailable(): Boolean {
return this == ScenarioUnavailabilityReason.None
}
}