Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-03 15:03:36 +05:00
parent 8b0f0e589e
commit f94370c41c
7 changed files with 139 additions and 62 deletions

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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<CryptoCurrency.Token>) {

View file

@ -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,
)
}

View file

@ -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<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val isUtxoConsolidationAvailableProvider: Provider<Boolean>,
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<ValidateAddressError, Unit>,
): 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(

View file

@ -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<CryptoCurrencyStatus>,
private val isUtxoConsolidationAvailableProvider: Provider<Boolean>,
) :
Converter<List<AvailableWallet?>, PersistentList<SendRecipientListContent>> {
override fun convert(value: List<AvailableWallet?>): PersistentList<SendRecipientListContent> {
return value.filterWallets().ifEmpty {
@ -20,8 +26,18 @@ internal class SendRecipientWalletListConverter :
private fun List<AvailableWallet?>.filterWallets(): PersistentList<SendRecipientListContent> {
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 }

View file

@ -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<AvailableWallet> = 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<UserWallet>.toAvailableWallets(): List<AvailableWallet> {
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<ValidateAddressError, Unit> = 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<GetFeeError, TransactionFee>? {
val isFromConfirmation = stateRouter.currentState.value.isFromConfirmation
val amountState = uiState.getAmountState(isFromConfirmation) ?: return null