diff --git a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt index 368216c920..c527434b1c 100644 --- a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 68ed536c3d..c75aefa33c 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -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, ) } diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt index ce11ee65f0..6527ffcacc 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt @@ -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, 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 { 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 - } - } - } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index b3ab55fbeb..57a6e2d52d 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -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 diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 4d18c1bfe9..429979f670 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -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 } + } } } diff --git a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt index ece1e97c5b..c9ef698068 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt @@ -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 suspend fun availableForSwap( @@ -46,4 +47,15 @@ interface RampStateManager { fun getSellInitializationStatus(): Flow> fun getExpressInitializationStatus(userWalletId: UserWalletId): Flow> + + /** + * 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 } \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingAvailability.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingAvailability.kt index 182d5fbec6..189d86172e 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingAvailability.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingAvailability.kt @@ -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() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index 1226448965..6db724ca58 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -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 { + 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 { 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 { - 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 { - 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() - val disabledList = mutableListOf() - - // 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, - disabledList: MutableList, - ) { - 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 { - val activeList = mutableListOf() - val disabledList = mutableListOf() - - 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 { - val activeList = mutableListOf() - val disabledList = mutableListOf() - 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 { + 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 { + return promoRepository.getStoryById(StoryContentIds.STORY_FIRST_TIME_SWAP.id) + .conflate() + .distinctUntilChanged() } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/ActionAvailabilityBuilder.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/ActionAvailabilityBuilder.kt new file mode 100644 index 0000000000..8b9bbfce83 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/ActionAvailabilityBuilder.kt @@ -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() + private val disabledList = mutableSetOf() + + /** 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.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 { + 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 { + val builder = ActionAvailabilityBuilder() + + builder.block() + + return builder.build() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt new file mode 100644 index 0000000000..16463ebb63 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt @@ -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?, + ) { + 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 + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt new file mode 100644 index 0000000000..fab3e19501 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt @@ -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 = 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?, + 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) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/MissedDerivationsActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/MissedDerivationsActionsFactory.kt new file mode 100644 index 0000000000..99bad15a0c --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/MissedDerivationsActionsFactory.kt @@ -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 { + val action = ActionState.HideToken(unavailabilityReason = ScenarioUnavailabilityReason.None) + + return setOf(action) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt new file mode 100644 index 0000000000..68530436ec --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt @@ -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 = 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 { + if (sources.networkSource != StatusSource.ACTUAL || reasonDeferred == null) { + return ScenarioUnavailabilityReason.UsedOutdatedData + } + + return reasonDeferred.await() + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt new file mode 100644 index 0000000000..9ccbd4b946 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt @@ -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 = + 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 + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index d58a0257f9..1807cc4a60 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -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 /** diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index 01c6a656e5..bdff6776cf 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -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 } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt index 2fb53e1132..b412ffc487 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt @@ -147,7 +147,7 @@ internal class OnrampTokenListModel @Inject constructor( private fun Lce.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( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 44c3e105b6..73e4fc5dc0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -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, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt index 81601a2e31..f87bae39fd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt @@ -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( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt index 22f8884acd..b59617acd1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt @@ -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