diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index bb6a62fc78..9635e67a29 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -11,15 +11,15 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton @Module -@InstallIn(ViewModelComponent::class) +@InstallIn(SingletonComponent::class) internal object TransactionDomainModule { @Provides - @ViewModelScoped + @Singleton fun provideGetFeeUseCase(walletManagersFacade: WalletManagersFacade): GetFeeUseCase { return GetFeeUseCase( walletManagersFacade = walletManagersFacade, @@ -28,7 +28,7 @@ internal object TransactionDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideSendTransactionUseCase( cardSdkConfigRepository: CardSdkConfigRepository, transactionRepository: TransactionRepository, @@ -43,7 +43,7 @@ internal object TransactionDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideAssociateAssetUseCase( cardSdkConfigRepository: CardSdkConfigRepository, walletManagersFacade: WalletManagersFacade, @@ -59,20 +59,28 @@ internal object TransactionDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideCreateTransactionUseCase(transactionRepository: TransactionRepository): CreateTransactionUseCase { return CreateTransactionUseCase(transactionRepository) } @Provides - @ViewModelScoped + @Singleton fun provideIsFeeApproximateUseCase(feeRepository: FeeRepository): IsFeeApproximateUseCase { return IsFeeApproximateUseCase(feeRepository) } @Provides - @ViewModelScoped + @Singleton fun provideValidateTransactionUseCase(transactionRepository: TransactionRepository): ValidateTransactionUseCase { return ValidateTransactionUseCase(transactionRepository) } + + @Provides + @Singleton + fun provideIsUtxoConsolidationAvailableUseCase( + walletManagersFacade: WalletManagersFacade, + ): IsUtxoConsolidationAvailableUseCase { + return IsUtxoConsolidationAvailableUseCase(walletManagersFacade) + } } \ No newline at end of file diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt b/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt index a6d7043d91..7a1b7db59c 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt @@ -2,9 +2,11 @@ package com.tangem.data.transaction.di import com.tangem.data.transaction.DefaultFeeRepository import com.tangem.data.transaction.DefaultTransactionRepository +import com.tangem.data.transaction.DefaultWalletAddressServiceRepository import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.transaction.WalletAddressServiceRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -36,4 +38,12 @@ internal object TransactionDataModule { fun providesFeeRepository(): FeeRepository { return DefaultFeeRepository() } + + @Provides + @Singleton + fun providesWalletAddressServiceRepository( + walletManagersFacade: WalletManagersFacade, + ): WalletAddressServiceRepository { + return DefaultWalletAddressServiceRepository(walletManagersFacade) + } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 205ea91ce0..6c5f931d5e 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -13,9 +13,9 @@ import com.tangem.blockchain.common.pagination.Page import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.common.trustlines.AssetRequirementsManager -import com.tangem.blockchain.common.txhistory.TransactionHistoryRequest import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.blockchain.transactionhistory.models.TransactionHistoryRequest import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.datasource.asset.loader.AssetLoader @@ -619,7 +619,7 @@ class DefaultWalletManagersFacade( derivationPath = network.derivationPath.value, ) ?: return false - return walletManager.allowUtxoConsolidation + return (walletManager as? UtxoBlockchainManager)?.allowConsolidation == true } private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set) { diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/IsUtxoConsolidationAvailableUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/IsUtxoConsolidationAvailableUseCase.kt new file mode 100644 index 0000000000..90839203b0 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/IsUtxoConsolidationAvailableUseCase.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.transaction.usecase + +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Gets UTXO consolidation availability + */ +class IsUtxoConsolidationAvailableUseCase( + private val walletManagersFacade: WalletManagersFacade, +) { + + suspend fun invokeSync(userWalletId: UserWalletId, network: Network) = + walletManagersFacade.checkUtxoConsolidationAvailability( + userWalletId = userWalletId, + network = network, + ) +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/RecipientSendFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/RecipientSendFactory.kt index 5fcb985783..c3fdbfd71b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/RecipientSendFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/RecipientSendFactory.kt @@ -1,11 +1,13 @@ package com.tangem.features.send.impl.presentation.state.recipient +import arrow.core.Either import arrow.core.getOrElse import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.txhistory.models.TxHistoryItem -import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase +import com.tangem.domain.transaction.error.ValidateAddressError +import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.state.SendUiState @@ -18,10 +20,14 @@ internal class RecipientSendFactory( private val stateRouterProvider: Provider, private val currentStateProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, + private val isUtxoConsolidationAvailableProvider: Provider, private val validateWalletMemoUseCase: ValidateWalletMemoUseCase, ) { private val recipientWalletListStateConverter by lazy(LazyThreadSafetyMode.NONE) { - SendRecipientWalletListConverter() + SendRecipientWalletListConverter( + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + isUtxoConsolidationAvailableProvider = isUtxoConsolidationAvailableProvider, + ) } private val recipientHistoryListStateConverter by lazy(LazyThreadSafetyMode.NONE) { SendRecipientHistoryListConverter( @@ -60,7 +66,10 @@ internal class RecipientSendFactory( ) } - fun getOnRecipientAddressValidState(value: String, isValidAddress: Boolean): SendUiState { + fun getOnRecipientAddressValidState( + value: String, + maybeValidAddress: Either, + ): SendUiState { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val state = currentStateProvider() val isEditState = stateRouterProvider().isEditState @@ -73,21 +82,28 @@ internal class RecipientSendFactory( Timber.e("Failed to validateWalletMemoUseCase: $it") false } - val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses - ?.any { it.value == value } ?: true return state.copyWrapped( isEditState = isEditState, recipientState = recipientState.copy( - isPrimaryButtonEnabled = isValidMemo && isValidAddress && !isAddressInWallet, + isPrimaryButtonEnabled = isValidMemo && maybeValidAddress.isRight(), isValidating = false, addressTextField = recipientState.addressTextField.copy( - error = when { - !isValidAddress -> resourceReference(R.string.send_recipient_address_error) - isAddressInWallet -> resourceReference(R.string.send_error_address_same_as_wallet) - else -> null - }, - isError = value.isNotEmpty() && !isValidAddress || isAddressInWallet, + error = maybeValidAddress.fold( + ifLeft = { + when (it) { + ValidateAddressError.InvalidAddress -> resourceReference( + R.string.send_recipient_address_error, + ) + ValidateAddressError.AddressInWallet -> resourceReference( + R.string.send_error_address_same_as_wallet, + ) + else -> null + } + }, + ifRight = { null }, + ), + isError = value.isNotEmpty() && maybeValidAddress.isLeft(), ), ), ) @@ -131,13 +147,11 @@ internal class RecipientSendFactory( Timber.e("Failed to validateWalletMemoUseCase: $it") false } - val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses - ?.any { it.value == value } ?: true return state.copyWrapped( isEditState = isEditState, recipientState = recipientState.copy( - isPrimaryButtonEnabled = isValidMemo && isValidAddress && !isAddressInWallet, + isPrimaryButtonEnabled = isValidMemo && isValidAddress, isValidating = false, memoTextField = recipientState.memoTextField?.copy( isError = value.isNotEmpty() && !isValidMemo, @@ -162,11 +176,10 @@ internal class RecipientSendFactory( ) } - fun getHiddenRecentListState(isAddressInWallet: Boolean, isValidAddress: Boolean): SendUiState { + fun getHiddenRecentListState(isNotValid: Boolean): SendUiState { val state = currentStateProvider() val isEditState = stateRouterProvider().isEditState val recipientState = state.getRecipientState(isEditState) ?: return state - val isNotValid = isAddressInWallet || !isValidAddress return state.copyWrapped( isEditState = isEditState, recipientState = recipientState.copy( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientWalletListConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientWalletListConverter.kt index 35d56f9846..6ac1a69d3f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientWalletListConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientWalletListConverter.kt @@ -1,16 +1,22 @@ package com.tangem.features.send.impl.presentation.state.recipient import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent import com.tangem.features.send.impl.presentation.state.recipient.utils.WALLET_DEFAULT_COUNT import com.tangem.features.send.impl.presentation.state.recipient.utils.WALLET_KEY_TAG import com.tangem.features.send.impl.presentation.state.recipient.utils.emptyListState +import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.toPersistentList -internal class SendRecipientWalletListConverter : +internal class SendRecipientWalletListConverter( + private val cryptoCurrencyStatusProvider: Provider, + private val isUtxoConsolidationAvailableProvider: Provider, +) : Converter, PersistentList> { override fun convert(value: List): PersistentList { return value.filterWallets().ifEmpty { @@ -20,8 +26,18 @@ internal class SendRecipientWalletListConverter : private fun List.filterWallets(): PersistentList { var walletsCounter = 0 + val currentAddress: String = runCatching { + cryptoCurrencyStatusProvider().value.networkAddress?.defaultAddress?.value + }.getOrNull().orEmpty() + return this.filterNotNull() - .filter { it.address.isNotBlank() } + .filter { + val isCoin = it.cryptoCurrency is CryptoCurrency.Coin + val isNotSameAddress = it.address != currentAddress + val isNotBlankAddress = it.address.isNotBlank() + + isNotBlankAddress && isCoin && (isNotSameAddress || isUtxoConsolidationAvailableProvider()) + } .groupBy { item -> item.name } .values.map { wallets -> val groupedByWallet = wallets.groupBy { it.userWalletId } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index d07fa04c51..bcf4954db8 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.* import arrow.core.Either import arrow.core.getOrElse +import arrow.core.left import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -34,10 +35,11 @@ import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.transaction.error.ValidateAddressError import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase -import com.tangem.domain.wallets.usecase.ValidateWalletAddressUseCase -import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase +import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase +import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource @@ -93,6 +95,7 @@ internal class SendViewModel @Inject constructor( private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase, + private val isUtxoConsolidationAvailableUseCase: IsUtxoConsolidationAvailableUseCase, @DelayedWork private val coroutineScope: CoroutineScope, validateTransactionUseCase: ValidateTransactionUseCase, currencyChecksRepository: CurrencyChecksRepository, @@ -135,6 +138,7 @@ internal class SendViewModel @Inject constructor( stateRouterProvider = Provider { stateRouter }, currentStateProvider = Provider { uiState }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + isUtxoConsolidationAvailableProvider = Provider { isUtxoConsolidationAvailable }, validateWalletMemoUseCase = validateWalletMemoUseCase, ) @@ -198,6 +202,7 @@ internal class SendViewModel @Inject constructor( private var userWallet: UserWallet by Delegates.notNull() private var userWallets: List = emptyList() private var isAmountSubtractAvailable: Boolean = false + private var isUtxoConsolidationAvailable: Boolean = false private var isTapHelpPreviewEnabled: Boolean = false private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() @@ -248,6 +253,7 @@ internal class SendViewModel @Inject constructor( ifRight = { wallet -> userWallet = wallet checkIfSubtractAvailable() + checkIfUtxoConsolidationAvailable() val isSingleWalletWithToken = wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() val isMultiCurrency = wallet.isMultiCurrency @@ -424,9 +430,6 @@ internal class SendViewModel @Inject constructor( } private suspend fun List.toAvailableWallets(): List { - val currentAddress: String = kotlin.runCatching { - cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value - }.getOrNull().orEmpty() return filterNot { it.isLocked } .mapNotNull { wallet -> val addresses = if (!wallet.isMultiCurrency) { @@ -440,16 +443,14 @@ internal class SendViewModel @Inject constructor( } else { getNetworkAddressesUseCase.invokeSync(wallet.walletId, cryptoCurrency.network) } - addresses - ?.filter { it.address != currentAddress && it.cryptoCurrency is CryptoCurrency.Coin } - ?.map { (cryptoCurrency, address) -> - AvailableWallet( - name = wallet.name, - address = address, - cryptoCurrency = cryptoCurrency, - userWalletId = wallet.walletId, - ) - } + addresses?.map { (cryptoCurrency, address) -> + AvailableWallet( + name = wallet.name, + address = address, + cryptoCurrency = cryptoCurrency, + userWalletId = wallet.walletId, + ) + } }.flatten() } @@ -624,8 +625,15 @@ internal class SendViewModel @Inject constructor( uiState = recipientStateFactory.getOnRecipientAddressValidationStarted() val isValidAddress = validateAddress(value) uiState = recipientStateFactory.getOnRecipientAddressValidState(value, isValidAddress) - type?.let { analyticsEventHandler.send(SendAnalyticEvents.AddressEntered(it, isValidAddress)) } - autoNextFromRecipient(type, isValidAddress) + type?.let { + analyticsEventHandler.send( + SendAnalyticEvents.AddressEntered( + it, + isValidAddress.isRight(), + ), + ) + } + autoNextFromRecipient(type, isValidAddress.isRight()) } }.saveIn(addressValidationJobHolder) } @@ -636,23 +644,22 @@ internal class SendViewModel @Inject constructor( uiState = recipientStateFactory.getOnRecipientMemoValueChange(value, isValuePasted) uiState = recipientStateFactory.getOnRecipientAddressValidationStarted() val recipientState = uiState.getRecipientState(stateRouter.isEditState) - val isValidAddress = validateAddress(recipientState?.addressTextField?.value.orEmpty()) - uiState = recipientStateFactory.getOnRecipientMemoValidState(value, isValidAddress) + val maybeValidAddress = validateAddress(recipientState?.addressTextField?.value.orEmpty()) + uiState = recipientStateFactory.getOnRecipientMemoValidState(value, maybeValidAddress.isRight()) } }.saveIn(memoValidationJobHolder) } - private suspend fun validateAddress(value: String): Boolean = runCatching { - val isValidAddress = validateWalletAddressUseCase( + private suspend fun validateAddress(value: String): Either = runCatching { + val maybeValidAddress = validateWalletAddressUseCase( userWalletId = userWalletId, network = cryptoCurrency.network, address = value, - ).getOrElse { false } - val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses - ?.any { it.value == value } ?: true - onEnteredValidAddress(isValidAddress, isAddressInWallet) - return isValidAddress - }.getOrElse { false } + currencyAddress = cryptoCurrencyStatus.value.networkAddress?.availableAddresses, + ) + onEnteredValidAddress(maybeValidAddress.isLeft()) + maybeValidAddress + }.getOrElse { ValidateAddressError.DataError(it).left() } private suspend fun checkIfXrpAddressValue(value: String): Boolean { return BlockchainUtils.decodeRippleXAddress(value, cryptoCurrency.network.id.value)?.let { decodedAddress -> @@ -665,11 +672,8 @@ internal class SendViewModel @Inject constructor( } ?: false } - private fun onEnteredValidAddress(isValidAddress: Boolean, isAddressInWallet: Boolean) { - uiState = recipientStateFactory.getHiddenRecentListState( - isAddressInWallet = isAddressInWallet, - isValidAddress = isValidAddress, - ) + private fun onEnteredValidAddress(isNotValid: Boolean) { + uiState = recipientStateFactory.getHiddenRecentListState(isNotValid = isNotValid) } private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean) { @@ -739,6 +743,13 @@ internal class SendViewModel @Inject constructor( ) } + private suspend fun checkIfUtxoConsolidationAvailable() { + isUtxoConsolidationAvailable = isUtxoConsolidationAvailableUseCase.invokeSync( + userWalletId = userWalletId, + network = cryptoCurrency.network, + ) + } + private suspend fun callFeeUseCase(): Either? { val isFromConfirmation = stateRouter.currentState.value.isFromConfirmation val amountState = uiState.getAmountState(isFromConfirmation) ?: return null