Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-11 22:34:54 +04:00
parent 309c9aad6c
commit 0e1e5e3a67
20 changed files with 834 additions and 484 deletions

View file

@ -5,7 +5,6 @@ import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -50,7 +49,6 @@ internal object ActivityModule {
appStateHolder: AppStateHolder,
expressServiceLoader: ExpressServiceLoader,
currenciesRepository: CurrenciesRepository,
getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase,
excludedBlockchains: ExcludedBlockchains,
dispatchers: CoroutineDispatcherProvider,
onrampFeatureToggles: OnrampFeatureToggles,
@ -61,7 +59,6 @@ internal object ActivityModule {
sellService = Provider { requireNotNull(appStateHolder.sellService) },
expressServiceLoader = expressServiceLoader,
currenciesRepository = currenciesRepository,
getNetworkCoinStatusUseCase = getNetworkCoinStatusUseCase,
excludedBlockchains = excludedBlockchains,
dispatchers = dispatchers,
onrampFeatureToggles = onrampFeatureToggles,

View file

@ -229,20 +229,16 @@ internal object TokensDomainModule {
fun provideGetCryptoCurrencyActionsUseCase(
rampStateManager: RampStateManager,
walletManagersFacade: WalletManagersFacade,
currenciesRepository: CurrenciesRepository,
stakingRepository: StakingRepository,
promoRepository: PromoRepository,
dispatchers: CoroutineDispatcherProvider,
currencyStatusOperations: BaseCurrencyStatusOperations,
): GetCryptoCurrencyActionsUseCase {
return GetCryptoCurrencyActionsUseCase(
rampManager = rampStateManager,
walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository,
stakingRepository = stakingRepository,
promoRepository = promoRepository,
dispatchers = dispatchers,
currencyStatusOperations = currencyStatusOperations,
)
}

View file

@ -14,11 +14,9 @@ import com.tangem.domain.exchange.ExpressAvailabilityState
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
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
@ -35,7 +33,6 @@ internal class DefaultRampManager(
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,
@ -56,20 +53,22 @@ internal class DefaultRampManager(
}
override suspend fun availableForSell(
userWallet: UserWallet,
userWalletId: UserWalletId,
status: CryptoCurrencyStatus,
sendUnavailabilityReason: ScenarioUnavailabilityReason?,
): Either<ScenarioUnavailabilityReason, Unit> {
return either {
val sellSupportedByService = catch(
block = {
val serviceCurrency = cryptoCurrencyConverter.convertBack(status.currency)
exchangeService?.availableForSell(currency = serviceCurrency) ?: false
exchangeService?.availableForSell(currency = serviceCurrency) == true
},
catch = { raise(ScenarioUnavailabilityReason.NotSupportedBySellService(status.currency.name)) },
)
val reason = getSendUnavailabilityReason(userWallet = userWallet, cryptoCurrencyStatus = status)
val reason = sendUnavailabilityReason
?: getSendUnavailabilityReason(userWalletId = userWalletId, cryptoCurrencyStatus = status)
ensure(condition = reason is ScenarioUnavailabilityReason.None) {
when (reason) {
@ -125,6 +124,27 @@ internal class DefaultRampManager(
return expressServiceLoader.getInitializationStatus(userWalletId)
}
override suspend fun getSendUnavailabilityReason(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): ScenarioUnavailabilityReason {
return when {
cryptoCurrencyStatus.value.amount.isNullOrZero() -> {
ScenarioUnavailabilityReason.EmptyBalance(ScenarioUnavailabilityReason.WithdrawalScenario.SEND)
}
currenciesRepository.isSendBlockedByPendingTransactions(
userWalletId = userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
) -> {
ScenarioUnavailabilityReason.PendingTransaction(
withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SEND,
networkName = cryptoCurrencyStatus.currency.network.name,
)
}
else -> ScenarioUnavailabilityReason.None
}
}
private suspend fun getExchangeableState(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
@ -204,33 +224,4 @@ internal class DefaultRampManager(
val contractAddress = (this as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE
return asset.network == network.backendId && asset.contractAddress.equals(contractAddress, ignoreCase = true)
}
private suspend fun getSendUnavailabilityReason(
userWallet: UserWallet,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): ScenarioUnavailabilityReason {
val coinStatus = getNetworkCoinStatusUseCase.invokeSync(
userWallet = userWallet,
networkId = cryptoCurrencyStatus.currency.network.id,
derivationPath = cryptoCurrencyStatus.currency.network.derivationPath,
).getOrNull()
return when {
cryptoCurrencyStatus.value.amount.isNullOrZero() -> {
ScenarioUnavailabilityReason.EmptyBalance(ScenarioUnavailabilityReason.WithdrawalScenario.SEND)
}
currenciesRepository.isSendBlockedByPendingTransactions(
cryptoCurrencyStatus = cryptoCurrencyStatus,
coinStatus = coinStatus,
) -> {
ScenarioUnavailabilityReason.PendingTransaction(
withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SEND,
networkName = coinStatus?.currency?.network?.name.orEmpty(),
)
}
else -> {
ScenarioUnavailabilityReason.None
}
}
}
}

View file

@ -235,7 +235,7 @@ internal class DefaultStakingRepository(
)
when {
prefetchedYield != null && isSupportedInMobileApp -> {
send(StakingAvailability.Available(prefetchedYield.id))
send(StakingAvailability.Available(prefetchedYield))
}
prefetchedYield == null && isSupportedInMobileApp -> {
send(StakingAvailability.TemporaryUnavailable)
@ -279,7 +279,7 @@ internal class DefaultStakingRepository(
return when {
prefetchedYield != null && isSupportedInMobileApp -> {
StakingAvailability.Available(prefetchedYield.id)
StakingAvailability.Available(prefetchedYield)
}
prefetchedYield == null && isSupportedInMobileApp -> {
StakingAvailability.TemporaryUnavailable

View file

@ -1,6 +1,7 @@
package com.tangem.data.tokens.repository
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionStatus
import com.tangem.blockchainsdk.compatibility.getL2CompatibilityTokenComparison
import com.tangem.blockchainsdk.utils.*
import com.tangem.data.common.api.safeApiCall
@ -432,9 +433,9 @@ internal class DefaultCurrenciesRepository(
}
}
override fun isSendBlockedByPendingTransactions(
override suspend fun isSendBlockedByPendingTransactions(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
coinStatus: CryptoCurrencyStatus?,
): Boolean {
val blockchain = cryptoCurrencyStatus.currency.network.toBlockchain()
val isBitcoinBlockchain = blockchain == Blockchain.Bitcoin || blockchain == Blockchain.BitcoinTestnet
@ -445,7 +446,14 @@ internal class DefaultCurrenciesRepository(
}
blockchain.isEvm() -> false
blockchain == Blockchain.Tron || blockchain == Blockchain.TronTestnet -> false
else -> coinStatus?.value?.hasCurrentNetworkTransactions == true
else -> {
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
network = cryptoCurrencyStatus.currency.network,
) ?: return false
walletManager.wallet.recentTransactions.any { it.status == TransactionStatus.Unconfirmed }
}
}
}

View file

@ -6,7 +6,6 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
@ -24,12 +23,14 @@ interface RampStateManager {
/**
* Check if [CryptoCurrency] is available for sell
*
* @param userWallet user wallet
* @param status crypto currency status
* @param userWalletId the ID of the user's wallet
* @param status crypto currency status
* @param sendUnavailabilityReason the reason why sending is unavailable or null
*/
suspend fun availableForSell(
userWallet: UserWallet,
userWalletId: UserWalletId,
status: CryptoCurrencyStatus,
sendUnavailabilityReason: ScenarioUnavailabilityReason?,
): Either<ScenarioUnavailabilityReason, Unit>
suspend fun availableForSwap(
@ -46,4 +47,15 @@ interface RampStateManager {
fun getSellInitializationStatus(): Flow<Lce<Throwable, Any>>
fun getExpressInitializationStatus(userWalletId: UserWalletId): Flow<Lce<Throwable, Any>>
/**
* Returns the reason why sending is unavailable for the given user wallet and cryptocurrency status
*
* @param userWalletId the ID of the user's wallet
* @param cryptoCurrencyStatus the status of the cryptocurrency
*/
suspend fun getSendUnavailabilityReason(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): ScenarioUnavailabilityReason
}

View file

@ -1,8 +1,10 @@
package com.tangem.domain.staking.model
import com.tangem.domain.staking.model.stakekit.Yield
sealed class StakingAvailability {
data class Available(val integrationId: String) : StakingAvailability()
data class Available(val yield: Yield) : StakingAvailability()
data object Unavailable : StakingAvailability()

View file

@ -1,463 +1,137 @@
package com.tangem.domain.tokens
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.promo.models.StoryContent
import com.tangem.domain.promo.models.StoryContentIds
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.actions.CommonActionsFactory
import com.tangem.domain.tokens.actions.MissedDerivationsActionsFactory
import com.tangem.domain.tokens.actions.OutdatedDataActionsFactory
import com.tangem.domain.tokens.actions.UnreachableActionsFactory
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.transaction.models.AssetRequirementsCondition
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.isNullOrZero
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withTimeoutOrNull
/**
* Use case to determine which TokenActions are available for a [CryptoCurrency]
* Use case for retrieving actions available for a specific cryptocurrency in a user's wallet.
*
* @property rampManager Ramp manager to check ramp availability
* @param rampManager the manager for handling ramp state operations
* @param walletManagersFacade the facade for managing wallet operations
* @property stakingRepository the repository for staking-related data
* @property promoRepository the repository for promotional content
* @property dispatchers the coroutine dispatcher provider for managing concurrency
*/
@Suppress("LongParameterList", "LargeClass")
class GetCryptoCurrencyActionsUseCase(
private val rampManager: RampStateManager,
private val walletManagersFacade: WalletManagersFacade,
private val currenciesRepository: CurrenciesRepository,
rampManager: RampStateManager,
walletManagersFacade: WalletManagersFacade,
private val stakingRepository: StakingRepository,
private val promoRepository: PromoRepository,
private val dispatchers: CoroutineDispatcherProvider,
private val currencyStatusOperations: BaseCurrencyStatusOperations,
) {
suspend operator fun invoke(
userWallet: UserWallet,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): Flow<TokenActionsState> {
private val unreachableActionsFactory = UnreachableActionsFactory(
walletManagersFacade = walletManagersFacade,
rampStateManager = rampManager,
)
private val outdatedDataActionsFactory = OutdatedDataActionsFactory(
walletManagersFacade = walletManagersFacade,
rampStateManager = rampManager,
)
private val commonActionsFactory = CommonActionsFactory(
walletManagersFacade = walletManagersFacade,
rampStateManager = rampManager,
)
operator fun invoke(userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus): Flow<TokenActionsState> {
return when (userWallet) {
is UserWallet.Cold -> {
coldFlow(userWallet, cryptoCurrencyStatus)
}
is UserWallet.Hot -> {
TODO("[REDACTED_TASK_KEY]")
}
is UserWallet.Cold -> coldFlow(userWallet, cryptoCurrencyStatus)
is UserWallet.Hot -> TODO("[REDACTED_TASK_KEY]")
}
}
private suspend fun coldFlow(
@OptIn(ExperimentalCoroutinesApi::class)
private fun coldFlow(
userWallet: UserWallet.Cold,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): Flow<TokenActionsState> {
val networkId = cryptoCurrencyStatus.currency.network.id
val requirements = withTimeoutOrNull(REQUEST_EXCHANGE_DATA_TIMEOUT) {
walletManagersFacade.getAssetRequirements(userWallet.walletId, cryptoCurrencyStatus.currency)
}
return flow {
val networkFlow = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
currencyStatusOperations.getNetworkCoinForSingleWalletWithTokenFlow(userWallet.walletId, networkId)
} else if (!userWallet.isMultiCurrency) {
currencyStatusOperations.getPrimaryCurrencyStatusFlow(userWallet.walletId, includeQuotes = false)
} else {
currencyStatusOperations.getNetworkCoinFlow(
userWalletId = userWallet.walletId,
networkId = networkId,
derivationPath = cryptoCurrencyStatus.currency.network.derivationPath,
includeQuotes = false,
)
}
val flow = combine(
flow = networkFlow,
flow2 = promoRepository.getStoryById(StoryContentIds.STORY_FIRST_TIME_SWAP.id).conflate(),
flow3 = stakingRepository.getStakingAvailability(
userWalletId = userWallet.walletId,
cryptoCurrency = cryptoCurrencyStatus.currency,
).onStart { emit(StakingAvailability.Unavailable) },
) { maybeCoinStatus, maybeSwapStories, stakingAvailability ->
createTokenActionsState(
userWallet = userWallet,
coinStatus = maybeCoinStatus.getOrNull(),
cryptoCurrencyStatus = cryptoCurrencyStatus,
requirements = requirements,
shouldShowSwapStories = maybeSwapStories != null,
isStakingAvailable = stakingAvailability is StakingAvailability.Available,
)
}
emitAll(flow)
}.flowOn(dispatchers.io)
}
private suspend fun createTokenActionsState(
userWallet: UserWallet,
coinStatus: CryptoCurrencyStatus?,
cryptoCurrencyStatus: CryptoCurrencyStatus,
requirements: AssetRequirementsCondition?,
shouldShowSwapStories: Boolean,
isStakingAvailable: Boolean,
): TokenActionsState {
return TokenActionsState(
walletId = userWallet.walletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
states = createListOfActions(
userWallet = userWallet,
coinStatus = coinStatus,
cryptoCurrencyStatus = cryptoCurrencyStatus,
requirements = requirements,
shouldShowSwapStories = shouldShowSwapStories,
isStakingAvailable = isStakingAvailable,
),
)
}
/**
* Creates list of action for expected order
* Actions priority: [Receive Send Swap Buy Sell]
*/
@Suppress("CyclomaticComplexMethod", "LongMethod")
private suspend fun createListOfActions(
userWallet: UserWallet,
coinStatus: CryptoCurrencyStatus?,
cryptoCurrencyStatus: CryptoCurrencyStatus,
requirements: AssetRequirementsCondition?,
shouldShowSwapStories: Boolean,
isStakingAvailable: Boolean,
): List<TokenActionsState.ActionState> {
val cryptoCurrency = cryptoCurrencyStatus.currency
if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.MissedDerivation) {
return listOf(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None))
}
if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Unreachable) {
return getActionsForUnreachableCurrency(userWallet, cryptoCurrencyStatus, requirements)
}
if (cryptoCurrencyStatus.value.sources.total != StatusSource.ACTUAL) {
return getActionsForOutdatedData(userWallet, cryptoCurrencyStatus, requirements, isStakingAvailable)
}
val activeList = mutableListOf<TokenActionsState.ActionState>()
val disabledList = mutableListOf<TokenActionsState.ActionState>()
// markets
// not a custom token
if (cryptoCurrencyStatus.currency.id.rawCurrencyId != null) {
activeList.add(TokenActionsState.ActionState.Analytics(ScenarioUnavailabilityReason.None))
}
// copy address
if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) {
activeList.add(TokenActionsState.ActionState.CopyAddress(ScenarioUnavailabilityReason.None))
}
// receive
if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) {
val scenario = getReceiveScenario(requirements)
activeList.add(TokenActionsState.ActionState.Receive(scenario))
}
// staking
addStakingActions(cryptoCurrency, isStakingAvailable, activeList, disabledList)
// send
val sendUnavailabilityReason = getSendUnavailabilityReason(
cryptoCurrencyStatus = cryptoCurrencyStatus,
coinStatus = coinStatus,
)
if (sendUnavailabilityReason == ScenarioUnavailabilityReason.None) {
activeList.add(TokenActionsState.ActionState.Send(sendUnavailabilityReason))
} else {
disabledList.add(TokenActionsState.ActionState.Send(sendUnavailabilityReason))
}
// swap
val swapActionState = getSwapUnavailabilityReason(userWallet, cryptoCurrencyStatus, shouldShowSwapStories)
if (swapActionState.unavailabilityReason == ScenarioUnavailabilityReason.None) {
activeList.add(swapActionState)
} else {
disabledList.add(swapActionState)
}
// buy
val onrampActionState = getOnrampUnavailabilityReason(userWallet, cryptoCurrencyStatus)
if (onrampActionState.unavailabilityReason == ScenarioUnavailabilityReason.None) {
activeList.add(onrampActionState)
} else {
disabledList.add(onrampActionState)
}
// region sell
rampManager.availableForSell(userWallet = userWallet, status = cryptoCurrencyStatus)
.onRight {
activeList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.None))
}
.onLeft { reason ->
disabledList.add(TokenActionsState.ActionState.Sell(reason))
}
// endregion
// hide
activeList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None))
return activeList + disabledList
}
private suspend fun addStakingActions(
cryptoCurrency: CryptoCurrency,
isStakingAvailable: Boolean,
activeList: MutableList<TokenActionsState.ActionState>,
disabledList: MutableList<TokenActionsState.ActionState>,
) {
if (isStakingAvailable) {
val yield = kotlin.runCatching {
stakingRepository.getYield(
cryptoCurrencyId = cryptoCurrency.id,
symbol = cryptoCurrency.symbol,
)
}.getOrNull()
activeList.add(
TokenActionsState.ActionState.Stake(
unavailabilityReason = ScenarioUnavailabilityReason.None,
yield = yield,
),
)
} else {
disabledList.add(
TokenActionsState.ActionState.Stake(
unavailabilityReason = ScenarioUnavailabilityReason.StakingUnavailable(cryptoCurrency.name),
yield = null,
),
)
}
}
private suspend fun getActionsForUnreachableCurrency(
userWallet: UserWallet,
cryptoCurrencyStatus: CryptoCurrencyStatus,
requirements: AssetRequirementsCondition?,
): List<TokenActionsState.ActionState> {
val activeList = mutableListOf<TokenActionsState.ActionState>()
val disabledList = mutableListOf<TokenActionsState.ActionState>()
if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) {
activeList.add(TokenActionsState.ActionState.CopyAddress(ScenarioUnavailabilityReason.None))
}
// buy (is not depend on cache)
val onrampActionState = getOnrampUnavailabilityReason(userWallet, cryptoCurrencyStatus)
if (onrampActionState.unavailabilityReason == ScenarioUnavailabilityReason.None) {
activeList.add(onrampActionState)
} else {
disabledList.add(onrampActionState)
}
disabledList.add(TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.Unreachable))
disabledList.add(
TokenActionsState.ActionState.Swap(
unavailabilityReason = ScenarioUnavailabilityReason.Unreachable,
showBadge = false,
),
)
disabledList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.Unreachable))
if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) {
val scenario = getReceiveScenario(requirements)
activeList.add(TokenActionsState.ActionState.Receive(scenario))
}
disabledList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.Unreachable, null))
activeList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None))
return activeList + disabledList
}
@Suppress("LongMethod")
private suspend fun getActionsForOutdatedData(
userWallet: UserWallet,
cryptoCurrencyStatus: CryptoCurrencyStatus,
requirements: AssetRequirementsCondition?,
isStakingAvailable: Boolean,
): List<TokenActionsState.ActionState> {
val activeList = mutableListOf<TokenActionsState.ActionState>()
val disabledList = mutableListOf<TokenActionsState.ActionState>()
val cryptoCurrency = cryptoCurrencyStatus.currency
// copy address
if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) {
activeList.add(TokenActionsState.ActionState.CopyAddress(ScenarioUnavailabilityReason.None))
}
// receive
if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) {
val scenario = getReceiveScenario(requirements)
val action = TokenActionsState.ActionState.Receive(scenario)
if (scenario == ScenarioUnavailabilityReason.None) {
activeList.add(action)
} else {
disabledList.add(action)
}
}
// swap
val sources = cryptoCurrencyStatus.value.sources
val isSwapAvailable = with(sources) {
quoteSource.isActual() && networkSource.isActual()
}
val swapAction = TokenActionsState.ActionState.Swap(
unavailabilityReason = if (isSwapAvailable) {
ScenarioUnavailabilityReason.None
} else if (sources.networkSource == StatusSource.ONLY_CACHE) {
ScenarioUnavailabilityReason.UsedOutdatedData
} else {
// CACHE source always when loading
ScenarioUnavailabilityReason.DataLoading
},
showBadge = false,
)
if (swapAction.unavailabilityReason == ScenarioUnavailabilityReason.None) {
activeList.add(swapAction)
} else {
disabledList.add(swapAction)
}
// buy (is not depend on cache)
val onrampActionState = getOnrampUnavailabilityReason(userWallet, cryptoCurrencyStatus)
if (onrampActionState.unavailabilityReason == ScenarioUnavailabilityReason.None) {
activeList.add(onrampActionState)
} else {
disabledList.add(onrampActionState)
}
// staking
if (cryptoCurrencyStatus.value.sources.networkSource.isActual()) {
addStakingActions(cryptoCurrency, isStakingAvailable, activeList, disabledList)
} else {
disabledList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.UsedOutdatedData, null))
}
// send
val isSendAvailable = cryptoCurrencyStatus.value.sources.networkSource.isActual()
val sendAction = TokenActionsState.ActionState.Send(
unavailabilityReason = if (isSendAvailable) {
ScenarioUnavailabilityReason.None
} else {
ScenarioUnavailabilityReason.UsedOutdatedData
},
)
if (sendAction.unavailabilityReason == ScenarioUnavailabilityReason.None) {
activeList.add(sendAction)
} else {
disabledList.add(sendAction)
}
// region sell
if (isSendAvailable) {
activeList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.None))
} else {
disabledList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.UsedOutdatedData))
}
// endregion
// hide
activeList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None))
return activeList + disabledList
}
private fun getReceiveScenario(requirements: AssetRequirementsCondition?): ScenarioUnavailabilityReason {
return when (requirements) {
AssetRequirementsCondition.PaidTransaction,
is AssetRequirementsCondition.PaidTransactionWithFee,
-> ScenarioUnavailabilityReason.UnassociatedAsset
is AssetRequirementsCondition.IncompleteTransaction,
null,
-> ScenarioUnavailabilityReason.None
is AssetRequirementsCondition.RequiredTrustline -> ScenarioUnavailabilityReason.TrustlineRequired
}
}
private fun getSendUnavailabilityReason(
cryptoCurrencyStatus: CryptoCurrencyStatus,
coinStatus: CryptoCurrencyStatus?,
): ScenarioUnavailabilityReason {
return when {
cryptoCurrencyStatus.value.amount.isNullOrZero() -> {
ScenarioUnavailabilityReason.EmptyBalance(ScenarioUnavailabilityReason.WithdrawalScenario.SEND)
cryptoCurrencyStatus.value is CryptoCurrencyStatus.MissedDerivation -> {
flowOf(value = MissedDerivationsActionsFactory.create())
}
currenciesRepository.isSendBlockedByPendingTransactions(
cryptoCurrencyStatus = cryptoCurrencyStatus,
coinStatus = coinStatus,
) -> {
ScenarioUnavailabilityReason.PendingTransaction(
withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SEND,
networkName = coinStatus?.currency?.network?.name.orEmpty(),
cryptoCurrencyStatus.value is CryptoCurrencyStatus.Unreachable -> {
flow {
val actions = unreachableActionsFactory.create(
userWallet = userWallet,
cryptoCurrencyStatus = cryptoCurrencyStatus,
)
emit(actions)
}
}
cryptoCurrencyStatus.value.sources.total != StatusSource.ACTUAL -> {
getStakingAvailabilityFlow(
userWalletId = userWallet.walletId,
currency = cryptoCurrencyStatus.currency,
)
.mapLatest {
outdatedDataActionsFactory.create(
userWallet = userWallet,
cryptoCurrencyStatus = cryptoCurrencyStatus,
stakingAvailability = it,
)
}
}
else -> {
ScenarioUnavailabilityReason.None
combine(
flow = getStakingAvailabilityFlow(
userWalletId = userWallet.walletId,
currency = cryptoCurrencyStatus.currency,
),
flow2 = getSwapStoryContent(),
) { stakingAvailability, swapStoryContent ->
commonActionsFactory.create(
userWallet = userWallet,
cryptoCurrencyStatus = cryptoCurrencyStatus,
stakingAvailability = stakingAvailability,
shouldShowSwapStories = swapStoryContent != null,
)
}
}
}
}
private suspend fun getSwapUnavailabilityReason(
userWallet: UserWallet,
cryptoCurrencyStatus: CryptoCurrencyStatus,
shouldShowSwapStories: Boolean,
): TokenActionsState.ActionState {
val cryptoCurrency = cryptoCurrencyStatus.currency
val isMultiCurrency =
userWallet is UserWallet.Hot || userWallet is UserWallet.Cold && userWallet.isMultiCurrency
return if (isMultiCurrency) {
if (cryptoCurrency.isCustom) {
return TokenActionsState.ActionState.Swap(
unavailabilityReason = ScenarioUnavailabilityReason.CustomToken(cryptoCurrency.name),
showBadge = false,
.map {
TokenActionsState(
walletId = userWallet.walletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
states = it.toList(),
)
}
if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.NoQuote) {
return TokenActionsState.ActionState.Swap(
unavailabilityReason = ScenarioUnavailabilityReason.TokenNoQuotes(cryptoCurrency.name),
showBadge = false,
)
}
val reason = rampManager.availableForSwap(userWallet.walletId, cryptoCurrency)
val isShowBadge = reason == ScenarioUnavailabilityReason.None && shouldShowSwapStories
TokenActionsState.ActionState.Swap(
unavailabilityReason = reason,
showBadge = isShowBadge,
)
} else {
TokenActionsState.ActionState.Swap(
unavailabilityReason = ScenarioUnavailabilityReason.SingleWallet,
showBadge = false,
)
}
.flowOn(dispatchers.default)
}
private suspend fun getOnrampUnavailabilityReason(
userWallet: UserWallet,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): TokenActionsState.ActionState {
userWallet.requireColdWallet() // TODO [REDACTED_TASK_KEY]
val cryptoCurrency = cryptoCurrencyStatus.currency
val reason = rampManager.availableForBuy(userWallet.scanResponse, userWallet.walletId, cryptoCurrency)
return TokenActionsState.ActionState.Buy(unavailabilityReason = reason)
private fun getStakingAvailabilityFlow(
userWalletId: UserWalletId,
currency: CryptoCurrency,
): Flow<StakingAvailability> {
return stakingRepository.getStakingAvailability(userWalletId = userWalletId, cryptoCurrency = currency)
.onStart { emit(StakingAvailability.Unavailable) }
.conflate()
.distinctUntilChanged()
}
private fun isAddressAvailable(networkAddress: NetworkAddress?): Boolean {
return networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty()
}
private companion object {
const val REQUEST_EXCHANGE_DATA_TIMEOUT = 1000L
private fun getSwapStoryContent(): Flow<StoryContent?> {
return promoRepository.getStoryById(StoryContentIds.STORY_FIRST_TIME_SWAP.id)
.conflate()
.distinctUntilChanged()
}
}

View file

@ -0,0 +1,62 @@
package com.tangem.domain.tokens.actions
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.TokenActionsState
/**
* Builder for creating a set of [TokenActionsState.ActionState] based on their availability
*
[REDACTED_AUTHOR]
*/
internal class ActionAvailabilityBuilder {
private val activeList = mutableSetOf<TokenActionsState.ActionState>()
private val disabledList = mutableSetOf<TokenActionsState.ActionState>()
/** Marks the current [TokenActionsState.ActionState] as active */
fun TokenActionsState.ActionState.active() {
activeList.add(this)
}
/** Marks the current [TokenActionsState.ActionState] as disabled */
fun TokenActionsState.ActionState.disabled() {
disabledList.add(this)
}
/** Marks a list of [TokenActionsState.ActionState] as disabled */
fun List<TokenActionsState.ActionState>.disabled() {
disabledList.addAll(this)
}
/**
* Adds the current [TokenActionsState.ActionState] to the appropriate list based on its [ScenarioUnavailabilityReason].
*
* If the [ScenarioUnavailabilityReason] is [ScenarioUnavailabilityReason.None], the action is added to the active list.
* Otherwise, it is added to the disabled list.
*/
fun TokenActionsState.ActionState.addByReason() {
if (unavailabilityReason == ScenarioUnavailabilityReason.None) {
activeList.add(this)
} else {
disabledList.add(this)
}
}
fun build(): Set<TokenActionsState.ActionState> {
return activeList + disabledList
}
}
/**
* This function initializes an [ActionAvailabilityBuilder], applies the given
* [block] to it, and returns the resulting set of [TokenActionsState.ActionState]
*/
internal suspend fun actionAvailabilityBuilder(
block: suspend ActionAvailabilityBuilder.() -> Unit,
): Set<TokenActionsState.ActionState> {
val builder = ActionAvailabilityBuilder()
builder.block()
return builder.build()
}

View file

@ -0,0 +1,188 @@
package com.tangem.domain.tokens.actions
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.TokenActionsState.ActionState
import com.tangem.domain.transaction.models.AssetRequirementsCondition
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.withTimeoutOrNull
/**
* Base factory class for creating token actions.
*
* This class provides utility methods to determine the availability of actions and to create specific token actions
* based on the provided conditions.
*
* @param walletManagersFacade the facade for managing wallet operations
* @param rampStateManager the manager for handling ramp state operations
*
[REDACTED_AUTHOR]
*/
internal open class BaseActionsFactory(
private val walletManagersFacade: WalletManagersFacade,
private val rampStateManager: RampStateManager,
) {
/** Checks if the provided network address [networkAddress] is available */
protected fun isAddressAvailable(networkAddress: NetworkAddress?): Boolean {
return networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty()
}
/**
* Retrieves the asset requirements for a specific user wallet and cryptocurrency.
*
* @param userWalletId The ID of the user wallet.
* @param currency The cryptocurrency to check.
* @return The asset requirements condition, or `null` if the operation times out.
*/
protected suspend fun getAssetRequirements(
userWalletId: UserWalletId,
currency: CryptoCurrency,
): AssetRequirementsCondition? {
return withTimeoutOrNull(timeMillis = 1000L) {
walletManagersFacade.getAssetRequirements(userWalletId = userWalletId, currency = currency)
}
}
/**
* Determines the unavailability reason for the BUY action
*
* @param userWallet the user's cold wallet
* @param currency the cryptocurrency to check
*/
protected suspend fun getOnrampUnavailabilityReason(
userWallet: UserWallet.Cold,
currency: CryptoCurrency,
): ScenarioUnavailabilityReason {
return rampStateManager.availableForBuy(
userWalletId = userWallet.walletId,
scanResponse = userWallet.scanResponse,
cryptoCurrency = currency,
)
}
/**
* Determines the unavailability reason for the SEND action
*
* @param userWalletId the ID of the user's wallet
* @param cryptoCurrencyStatus the status of the cryptocurrency
*/
protected suspend fun getSendUnavailabilityReason(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): ScenarioUnavailabilityReason {
return rampStateManager.getSendUnavailabilityReason(
userWalletId = userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
)
}
/**
* Determines the unavailability reason for the SELL action
*
* @param userWalletId the ID of the user's wallet
* @param status the status of the cryptocurrency
* @param sendUnavailabilityReason the reason for unavailability of the send action
*/
protected suspend fun getSellUnavailabilityReason(
userWalletId: UserWalletId,
status: CryptoCurrencyStatus,
sendUnavailabilityReason: ScenarioUnavailabilityReason,
): ScenarioUnavailabilityReason {
return rampStateManager.availableForSell(
userWalletId = userWalletId,
status = status,
sendUnavailabilityReason = sendUnavailabilityReason,
).fold(
ifLeft = { it },
ifRight = { ScenarioUnavailabilityReason.None },
)
}
/** Adds a "Copy Address" action to the builder if the address is available [isAddressAvailable] */
protected fun ActionAvailabilityBuilder.addCopyAction(isAddressAvailable: Boolean) {
if (isAddressAvailable) {
ActionState.CopyAddress(unavailabilityReason = ScenarioUnavailabilityReason.None).active()
}
}
/**
* Adds a "Receive" action to the builder based on the address availability and asset requirements
*
* @param isAddressAvailable indicates whether the address is available
* @param requirementsDeferred a deferred object containing the asset requirements condition
*/
protected suspend fun ActionAvailabilityBuilder.addReceiveAction(
isAddressAvailable: Boolean,
requirementsDeferred: Deferred<AssetRequirementsCondition?>?,
) {
if (isAddressAvailable && requirementsDeferred != null) {
val scenario = getReceiveScenario(requirements = requirementsDeferred.await())
val action = ActionState.Receive(scenario)
if (scenario == ScenarioUnavailabilityReason.None) {
action.active()
} else {
action.disabled()
}
}
}
/** Adds a "Buy" action to the builder based on the unavailability [reason] */
protected fun ActionAvailabilityBuilder.addBuyAction(reason: ScenarioUnavailabilityReason) {
val action = ActionState.Buy(unavailabilityReason = reason)
if (reason == ScenarioUnavailabilityReason.None) {
action.active()
} else {
action.disabled()
}
}
/** Adds a "Hide Token" action to the builder */
protected fun ActionAvailabilityBuilder.addHideTokenAction() {
ActionState.HideToken(unavailabilityReason = ScenarioUnavailabilityReason.None).active()
}
/**
* Creates a staking action based on the staking availability
*
* @param currency the cryptocurrency for staking
* @param stakingAvailability the staking availability status
*/
protected fun createStakingAction(
currency: CryptoCurrency,
stakingAvailability: StakingAvailability,
): ActionState.Stake {
return if (stakingAvailability is StakingAvailability.Available) {
ActionState.Stake(
unavailabilityReason = ScenarioUnavailabilityReason.None,
yield = stakingAvailability.yield,
)
} else {
ActionState.Stake(
unavailabilityReason = ScenarioUnavailabilityReason.StakingUnavailable(currency.name),
yield = null,
)
}
}
private fun getReceiveScenario(requirements: AssetRequirementsCondition?): ScenarioUnavailabilityReason {
return when (requirements) {
AssetRequirementsCondition.PaidTransaction,
is AssetRequirementsCondition.PaidTransactionWithFee,
-> ScenarioUnavailabilityReason.UnassociatedAsset
is AssetRequirementsCondition.IncompleteTransaction,
null,
-> ScenarioUnavailabilityReason.None
is AssetRequirementsCondition.RequiredTrustline -> ScenarioUnavailabilityReason.TrustlineRequired
}
}
}

View file

@ -0,0 +1,176 @@
package com.tangem.domain.tokens.actions
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.TokenActionsState.ActionState
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
/**
* Factory class for creating common token actions
*
* @param walletManagersFacade the facade for managing wallet operations
* @param rampStateManager the manager for handling ramp state operations
*
[REDACTED_AUTHOR]
*/
internal class CommonActionsFactory(
walletManagersFacade: WalletManagersFacade,
private val rampStateManager: RampStateManager,
) : BaseActionsFactory(walletManagersFacade, rampStateManager) {
/**
* Creates a set of token actions based on the provided parameters
*
* @param userWallet the user's cold wallet
* @param cryptoCurrencyStatus the status of the cryptocurrency
* @param stakingAvailability the staking availability for the cryptocurrency
* @param shouldShowSwapStories a flag indicating whether to show swap stories
*/
suspend fun create(
userWallet: UserWallet.Cold,
cryptoCurrencyStatus: CryptoCurrencyStatus,
stakingAvailability: StakingAvailability,
shouldShowSwapStories: Boolean,
): Set<ActionState> = coroutineScope {
val isAddressAvailable = isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)
val requirementsDeferred = if (isAddressAvailable) {
async {
getAssetRequirements(userWalletId = userWallet.walletId, currency = cryptoCurrencyStatus.currency)
}
} else {
null
}
val onrampUnavailabilityReasonDeferred = async {
getOnrampUnavailabilityReason(userWallet = userWallet, currency = cryptoCurrencyStatus.currency)
}
val sendUnavailabilityReasonDeferred = async {
getSendUnavailabilityReason(userWalletId = userWallet.walletId, cryptoCurrencyStatus = cryptoCurrencyStatus)
}
val swapUnavailabilityReason = if (!cryptoCurrencyStatus.currency.isCustom &&
cryptoCurrencyStatus.value !is CryptoCurrencyStatus.NoQuote
) {
async {
getSwapUnavailabilityReason(
userWalletId = userWallet.walletId,
currency = cryptoCurrencyStatus.currency,
)
}
} else {
null
}
actionAvailabilityBuilder {
// region Analytics
if (cryptoCurrencyStatus.currency.id.rawCurrencyId != null) {
ActionState.Analytics(unavailabilityReason = ScenarioUnavailabilityReason.None).active()
}
// endregion
// region Copy
addCopyAction(isAddressAvailable = isAddressAvailable)
// endregion
// region Receive
addReceiveAction(isAddressAvailable = isAddressAvailable, requirementsDeferred = requirementsDeferred)
// endregion
// region Stake
createStakingAction(currency = cryptoCurrencyStatus.currency, stakingAvailability = stakingAvailability)
.addByReason()
// endregion
val sendUnavailabilityReason = sendUnavailabilityReasonDeferred.await()
// region Send
ActionState.Send(unavailabilityReason = sendUnavailabilityReason).addByReason()
// endregion
// region Swap
createSwapAction(
userWallet = userWallet,
cryptoCurrencyStatus = cryptoCurrencyStatus,
swapUnavailableReasonDeferred = swapUnavailabilityReason,
shouldShowSwapStories = shouldShowSwapStories,
).addByReason()
// endregion
// region Buy
addBuyAction(reason = onrampUnavailabilityReasonDeferred.await())
// endregion
// region Sell
val sellUnavailabilityReason = getSellUnavailabilityReason(
userWalletId = userWallet.walletId,
status = cryptoCurrencyStatus,
sendUnavailabilityReason = sendUnavailabilityReason,
)
ActionState.Sell(unavailabilityReason = sellUnavailabilityReason).addByReason()
// endregion
// region HideToken
addHideTokenAction()
// endregion
}
}
private suspend fun createSwapAction(
userWallet: UserWallet,
cryptoCurrencyStatus: CryptoCurrencyStatus,
swapUnavailableReasonDeferred: Deferred<ScenarioUnavailabilityReason>?,
shouldShowSwapStories: Boolean,
): ActionState {
val cryptoCurrency = cryptoCurrencyStatus.currency
val isMultiCurrency = userWallet is UserWallet.Cold && userWallet.isMultiCurrency ||
userWallet is UserWallet.Hot
if (!isMultiCurrency) {
return ActionState.Swap(
unavailabilityReason = ScenarioUnavailabilityReason.SingleWallet,
showBadge = false,
)
}
return when {
cryptoCurrency.isCustom -> {
ActionState.Swap(
unavailabilityReason = ScenarioUnavailabilityReason.CustomToken(cryptoCurrency.name),
showBadge = false,
)
}
cryptoCurrencyStatus.value is CryptoCurrencyStatus.NoQuote -> {
ActionState.Swap(
unavailabilityReason = ScenarioUnavailabilityReason.TokenNoQuotes(cryptoCurrency.name),
showBadge = false,
)
}
else -> {
val reason = swapUnavailableReasonDeferred!!.await()
return ActionState.Swap(
unavailabilityReason = reason,
showBadge = reason == ScenarioUnavailabilityReason.None && shouldShowSwapStories,
)
}
}
}
private suspend fun getSwapUnavailabilityReason(
userWalletId: UserWalletId,
currency: CryptoCurrency,
): ScenarioUnavailabilityReason {
return rampStateManager.availableForSwap(userWalletId = userWalletId, cryptoCurrency = currency)
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.domain.tokens.actions
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.TokenActionsState.ActionState
/**
* Factory for creating a set of token action states for missed derivations
*
[REDACTED_AUTHOR]
*/
internal object MissedDerivationsActionsFactory {
/** Creates a set of token actions */
fun create(): Set<ActionState> {
val action = ActionState.HideToken(unavailabilityReason = ScenarioUnavailabilityReason.None)
return setOf(action)
}
}

View file

@ -0,0 +1,154 @@
package com.tangem.domain.tokens.actions
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.models.StatusSource
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.TokenActionsState.ActionState
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
/**
* Factory for creating a set of token action states when data is outdated
*
* @param walletManagersFacade the facade for managing wallet operations
* @param rampStateManager the manager for handling ramp state operations
*
[REDACTED_AUTHOR]
*/
internal class OutdatedDataActionsFactory(
walletManagersFacade: WalletManagersFacade,
rampStateManager: RampStateManager,
) : BaseActionsFactory(walletManagersFacade, rampStateManager) {
/**
* Creates a set of token actions based on the provided parameters
*
* @param userWallet the user's cold wallet
* @param cryptoCurrencyStatus the status of the cryptocurrency
* @param stakingAvailability the staking availability for the cryptocurrency
*/
suspend fun create(
userWallet: UserWallet.Cold,
cryptoCurrencyStatus: CryptoCurrencyStatus,
stakingAvailability: StakingAvailability,
): Set<ActionState> = coroutineScope {
val sources = cryptoCurrencyStatus.value.sources
val isAddressAvailable = isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)
val requirementsDeferred = if (isAddressAvailable) {
async {
getAssetRequirements(userWalletId = userWallet.walletId, currency = cryptoCurrencyStatus.currency)
}
} else {
null
}
val onrampUnavailabilityReasonDeferred = async {
getOnrampUnavailabilityReason(userWallet = userWallet, currency = cryptoCurrencyStatus.currency)
}
val sendUnavailabilityReasonDeferred = if (sources.networkSource == StatusSource.ACTUAL) {
async {
getSendUnavailabilityReason(
userWalletId = userWallet.walletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
)
}
} else {
null
}
actionAvailabilityBuilder {
// region Copy
addCopyAction(isAddressAvailable = isAddressAvailable)
// endregion
// region Receive
addReceiveAction(isAddressAvailable = isAddressAvailable, requirementsDeferred = requirementsDeferred)
// endregion
// region Swap
createSwapAction(sources = sources).addByReason()
// endregion
// region Buy
addBuyAction(reason = onrampUnavailabilityReasonDeferred.await())
// endregion
// region Stake
if (sources.networkSource.isActual()) {
val stakingAction = createStakingAction(
currency = cryptoCurrencyStatus.currency,
stakingAvailability = stakingAvailability,
)
stakingAction.addByReason()
} else {
val stakingAction = ActionState.Stake(
unavailabilityReason = ScenarioUnavailabilityReason.UsedOutdatedData,
yield = null,
)
stakingAction.disabled()
}
// endregion
val sendUnavailabilityReason = getSendUnavailabilityReason(
sources = sources,
reasonDeferred = sendUnavailabilityReasonDeferred,
)
// region Send
ActionState.Send(sendUnavailabilityReason).addByReason()
// endregion
// region Sell
if (sendUnavailabilityReason == ScenarioUnavailabilityReason.None) {
val sellUnavailabilityReason = getSellUnavailabilityReason(
userWalletId = userWallet.walletId,
status = cryptoCurrencyStatus,
sendUnavailabilityReason = sendUnavailabilityReason,
)
ActionState.Sell(sellUnavailabilityReason).addByReason()
} else {
ActionState.Sell(sendUnavailabilityReason).disabled()
}
// endregion
// region HideToken
addHideTokenAction()
// endregion
}
}
private fun createSwapAction(sources: CryptoCurrencyStatus.Sources): ActionState {
val isSwapAvailable = with(sources) { quoteSource.isActual() && networkSource.isActual() }
return ActionState.Swap(
unavailabilityReason = when {
isSwapAvailable -> ScenarioUnavailabilityReason.None
sources.networkSource == StatusSource.ONLY_CACHE -> ScenarioUnavailabilityReason.UsedOutdatedData
else -> ScenarioUnavailabilityReason.DataLoading // CACHE source always when loading
},
showBadge = false,
)
}
private suspend fun getSendUnavailabilityReason(
sources: CryptoCurrencyStatus.Sources,
reasonDeferred: Deferred<ScenarioUnavailabilityReason>?,
): ScenarioUnavailabilityReason {
if (sources.networkSource != StatusSource.ACTUAL || reasonDeferred == null) {
return ScenarioUnavailabilityReason.UsedOutdatedData
}
return reasonDeferred.await()
}
}

View file

@ -0,0 +1,73 @@
package com.tangem.domain.tokens.actions
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.TokenActionsState.ActionState
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
/**
* Factory for creating a set of unreachable token action states
*
* @param walletManagersFacade the facade for managing wallet operations
* @param rampStateManager the manager for handling ramp state operations
*
[REDACTED_AUTHOR]
*/
internal class UnreachableActionsFactory(
walletManagersFacade: WalletManagersFacade,
rampStateManager: RampStateManager,
) : BaseActionsFactory(walletManagersFacade, rampStateManager) {
suspend fun create(userWallet: UserWallet.Cold, cryptoCurrencyStatus: CryptoCurrencyStatus): Set<ActionState> =
coroutineScope {
val isAddressAvailable = isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)
// region Deferred
val requirementsDeferred = if (isAddressAvailable) {
async {
getAssetRequirements(userWalletId = userWallet.walletId, currency = cryptoCurrencyStatus.currency)
}
} else {
null
}
val onrampUnavailabilityReasonDeferred = async {
getOnrampUnavailabilityReason(userWallet = userWallet, currency = cryptoCurrencyStatus.currency)
}
// endregion
actionAvailabilityBuilder {
// region Copy
addCopyAction(isAddressAvailable = isAddressAvailable)
// endregion
// region Buy
addBuyAction(reason = onrampUnavailabilityReasonDeferred.await())
// endregion
// region Receive
addReceiveAction(isAddressAvailable = isAddressAvailable, requirementsDeferred = requirementsDeferred)
// endregion
// region Send, Swap, Sell, Stake
listOf(
ActionState.Send(unavailabilityReason = ScenarioUnavailabilityReason.Unreachable),
ActionState.Swap(
unavailabilityReason = ScenarioUnavailabilityReason.Unreachable,
showBadge = false,
),
ActionState.Sell(unavailabilityReason = ScenarioUnavailabilityReason.Unreachable),
ActionState.Stake(unavailabilityReason = ScenarioUnavailabilityReason.Unreachable, yield = null),
).disabled()
// endregion
// region HideToken
addHideTokenAction()
// endregion
}
}
}

View file

@ -223,12 +223,12 @@ interface CurrenciesRepository {
/**
* Determines whether the currency sending is blocked by network pending transaction
*
* @param userWalletId the unique identifier of the user wallet
* @param cryptoCurrencyStatus currency status
* @param coinStatus main currency status in [cryptoCurrencyStatus] network
*/
fun isSendBlockedByPendingTransactions(
suspend fun isSendBlockedByPendingTransactions(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
coinStatus: CryptoCurrencyStatus?,
): Boolean
/**

View file

@ -134,9 +134,9 @@ internal class MockCurrenciesRepository(
return isSortedByBalance.map { it.getOrElse { e -> throw e } }
}
override fun isSendBlockedByPendingTransactions(
override suspend fun isSendBlockedByPendingTransactions(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
coinStatus: CryptoCurrencyStatus?,
): Boolean {
return false
}

View file

@ -147,7 +147,7 @@ internal class OnrampTokenListModel @Inject constructor(
private fun Lce<TokenListError, TokenList>.isInsufficientBalanceForSell(): Boolean {
return if (params.filterOperation == OnrampOperation.SELL) {
isContent {
(it.totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isZero() ?: false
(it.totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isZero() == true
}
} else {
false
@ -235,7 +235,11 @@ internal class OnrampTokenListModel @Inject constructor(
).isAvailable()
}
OnrampOperation.SELL -> {
rampStateManager.availableForSell(userWallet = userWallet, status = status).isRight()
rampStateManager.availableForSell(
userWalletId = userWallet.walletId,
status = status,
sendUnavailabilityReason = null,
).isRight()
}
OnrampOperation.SWAP -> {
val isAvailable = rampStateManager.availableForSwap(

View file

@ -257,7 +257,7 @@ internal class TokenDetailsModel @Inject constructor(
.launchIn(modelScope)
}
private suspend fun updateButtons(currencyStatus: CryptoCurrencyStatus) {
private fun updateButtons(currencyStatus: CryptoCurrencyStatus) {
getCryptoCurrencyActionsUseCase(
userWallet = userWallet,
cryptoCurrencyStatus = currencyStatus,

View file

@ -11,17 +11,11 @@ import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarningsFactory
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.subscribers.*
import com.tangem.feature.wallet.presentation.wallet.subscribers.PrimaryCurrencySubscriber
import com.tangem.feature.wallet.presentation.wallet.subscribers.SingleWalletButtonsSubscriber
import com.tangem.feature.wallet.presentation.wallet.subscribers.SingleWalletNotificationsSubscriber
import com.tangem.feature.wallet.presentation.wallet.subscribers.TxHistorySubscriber
import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletDropDownItemsSubscriber
import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber
@Suppress("LongParameterList")
internal class SingleWalletContentLoader(

View file

@ -12,10 +12,10 @@ import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarningsFactory
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import javax.inject.Inject
@ModelScoped