From 8c6372e398568ae370958ab315e4dc34bd4a4ca4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 Dec 2023 18:50:39 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/di/domain/WalletsDomainModule.kt | 8 +++ .../repository/DefaultTxHistoryRepository.kt | 7 ++- .../DefaultWalletAddressServiceRepository.kt | 41 ++++++++++++- .../domain/transaction/error/GetFeeError.kt | 4 +- .../transaction/usecase/GetFeeUseCase.kt | 28 +++++---- .../WalletAddressServiceRepository.kt | 4 +- .../usecase/ValidateWalletAddressUseCase.kt | 2 +- .../usecase/ValidateWalletMemoUseCase.kt | 17 ++++++ features/send/impl/build.gradle.kts | 1 + .../presentation/state/SendStateFactory.kt | 57 ++++++++++--------- .../presentation/ui/SendNavigationButtons.kt | 10 ++-- .../viewmodel/AddressVerification.kt | 18 ------ .../presentation/viewmodel/SendViewModel.kt | 21 ++----- 13 files changed, 136 insertions(+), 82 deletions(-) create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ValidateWalletMemoUseCase.kt delete mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/AddressVerification.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 9efdcc16d8..809e962e12 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -105,4 +105,12 @@ internal object WalletsDomainModule { dispatchers = dispatchers, ) } + + @Provides + @ViewModelScoped + fun providesValidateWalletMemoUseCase( + walletAddressServiceRepository: WalletAddressServiceRepository, + ): ValidateWalletMemoUseCase { + return ValidateWalletMemoUseCase(walletAddressServiceRepository = walletAddressServiceRepository) + } } \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt index c36eb493ed..9120a85ef1 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt @@ -66,7 +66,12 @@ class DefaultTxHistoryRepository( override fun getTxExploreUrl(txHash: String, networkId: Network.ID): String { val blockchain = Blockchain.fromId(networkId.value) - return blockchain.getExploreTxUrl(txHash) + // TODO: Fix ton tx urls [REDACTED_TASK_KEY] + return if (blockchain == Blockchain.TON || blockchain == Blockchain.TONTestnet) { + "" + } else { + blockchain.getExploreTxUrl(txHash) + } } private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet { diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletAddressServiceRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletAddressServiceRepository.kt index 689f2e9c63..09f2eb0835 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletAddressServiceRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletAddressServiceRepository.kt @@ -1,16 +1,19 @@ package com.tangem.data.wallets +import androidx.core.text.isDigitsOnly import com.tangem.blockchain.blockchains.near.NearWalletManager import com.tangem.blockchain.common.Blockchain import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.repository.WalletAddressServiceRepository +import java.math.BigInteger class DefaultWalletAddressServiceRepository( private val walletManagersFacade: WalletManagersFacade, ) : WalletAddressServiceRepository { - override suspend fun validate(userWalletId: UserWalletId, network: Network, address: String): Boolean { + + override suspend fun validateAddress(userWalletId: UserWalletId, network: Network, address: String): Boolean { val blockchain = Blockchain.fromId(network.id.value) return if (blockchain.isNear()) { @@ -25,7 +28,43 @@ class DefaultWalletAddressServiceRepository( } } + override fun validateMemo(network: Network, memo: String): Boolean { + if (memo.isEmpty()) return true + return when (network.id.value) { + Blockchain.XRP.id -> { + val tag = memo.toLongOrNull() + tag != null && tag <= XRP_TAG_MAX_NUMBER + } + Blockchain.Stellar.id -> { + isAssignableXlmValue(memo) + } + else -> true + } + } + private fun Blockchain.isNear(): Boolean { return this == Blockchain.Near || this == Blockchain.NearTestnet } + + private fun isAssignableXlmValue(value: String): Boolean { + return when { + value.isNotEmpty() && value.isDigitsOnly() -> { + try { + // from com.tangem.blockchain.blockchains.stellar.StellarMemo.toStellarSdkMemo + value.toBigInteger() in BigInteger.ZERO..Long.MAX_VALUE.toBigInteger() * 2.toBigInteger() + } catch (ex: NumberFormatException) { + false + } + } + else -> { + // from org.stellar.sdk.MemoText + value.toByteArray().size <= XLM_MEMO_MAX_LENGTH + } + } + } + + companion object { + private const val XLM_MEMO_MAX_LENGTH = 28 + private const val XRP_TAG_MAX_NUMBER = 4294967295 + } } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt index f08011b2d4..e6669368f9 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt @@ -1,5 +1,7 @@ package com.tangem.domain.transaction.error sealed class GetFeeError { - object DataError : GetFeeError() + data class DataError(val cause: Throwable?) : GetFeeError() + + object UnknownError : GetFeeError() } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt index dee4c3cdce..e27a7d2625 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt @@ -30,20 +30,26 @@ class GetFeeUseCase( destination: String, userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, - ): Flow> { + ): Flow> { return flow { - val result = walletManagersFacade.getFee( - amount = convertCryptoCurrencyToAmount(cryptoCurrency, amount), - destination = destination, - userWalletId = userWalletId, - network = cryptoCurrency.network, - ) + try { + val result = requireNotNull( + walletManagersFacade.getFee( + amount = convertCryptoCurrencyToAmount(cryptoCurrency, amount), + destination = destination, + userWalletId = userWalletId, + network = cryptoCurrency.network, + ), + ) { "Fee is null" } - val maybeFee = when (result) { - is Result.Success -> result.data.right() - else -> GetFeeError.DataError.left() + val maybeFee = when (result) { + is Result.Success -> result.data.right() + is Result.Failure -> GetFeeError.DataError(result.error).left() + } + emit(maybeFee) + } catch (e: Exception) { + emit(GetFeeError.DataError(e.cause).left()) } - emit(maybeFee) }.flowOn(dispatcher.io) } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletAddressServiceRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletAddressServiceRepository.kt index a0d42d07b5..1d36444caf 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletAddressServiceRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletAddressServiceRepository.kt @@ -8,5 +8,7 @@ import com.tangem.domain.wallets.models.UserWalletId */ interface WalletAddressServiceRepository { - suspend fun validate(userWalletId: UserWalletId, network: Network, address: String): Boolean + suspend fun validateAddress(userWalletId: UserWalletId, network: Network, address: String): Boolean + + fun validateMemo(network: Network, memo: String): Boolean } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ValidateWalletAddressUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ValidateWalletAddressUseCase.kt index 3e47fedabc..149b12030d 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ValidateWalletAddressUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ValidateWalletAddressUseCase.kt @@ -21,7 +21,7 @@ class ValidateWalletAddressUseCase( address: String, ): Either = withContext(dispatchers.io) { Either.catch { - walletAddressServiceRepository.validate(userWalletId, network, address) + walletAddressServiceRepository.validateAddress(userWalletId, network, address) } } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ValidateWalletMemoUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ValidateWalletMemoUseCase.kt new file mode 100644 index 0000000000..754819852b --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ValidateWalletMemoUseCase.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.repository.WalletAddressServiceRepository + +/** + * Use case for validating wallet memo. + */ +class ValidateWalletMemoUseCase( + private val walletAddressServiceRepository: WalletAddressServiceRepository, +) { + + operator fun invoke(network: Network, memo: String): Either = Either.catch { + walletAddressServiceRepository.validateMemo(network, memo) + } +} \ No newline at end of file diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 69d2d12a3f..d0bf0f05b7 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(deps.arrow.core) implementation(deps.lifecycle.compose) implementation(deps.jodatime) + implementation(deps.timber) /** Compose */ implementation(deps.compose.accompanist.systemUiController) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index 1f03dddf54..becf5056a6 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -1,7 +1,7 @@ package com.tangem.features.send.impl.presentation.state import androidx.paging.PagingData -import com.tangem.blockchain.common.address.Address +import arrow.core.getOrElse import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.resourceReference @@ -10,33 +10,32 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.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.amount.SendAmountStateConverter import com.tangem.features.send.impl.presentation.state.fee.* import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter -import com.tangem.features.send.impl.presentation.state.fee.calculateReceiveAmount import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientListConverter import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientStateConverter import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents -import com.tangem.features.send.impl.presentation.viewmodel.isNotAddressInWallet -import com.tangem.features.send.impl.presentation.viewmodel.validateMemo import com.tangem.utils.Provider import com.tangem.utils.isNullOrZero import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow +import timber.log.Timber import java.math.BigDecimal -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") internal class SendStateFactory( private val clickIntents: SendClickIntents, private val currentStateProvider: Provider, private val userWalletProvider: Provider, - private val walletAddressesProvider: Provider>, private val appCurrencyProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, + private val validateWalletMemoUseCase: ValidateWalletMemoUseCase, coinCryptoCurrencyStatusProvider: Provider, ) { @@ -138,29 +137,32 @@ internal class SendStateFactory( } fun getOnRecipientAddressValidState(value: String, isValidAddress: Boolean): SendUiState { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val state = currentStateProvider() val recipientState = state.recipientState ?: return state - val isValidMemo = validateMemo( - memo = recipientState.addressTextField.value, - cryptoCurrency = cryptoCurrencyStatusProvider().currency, - ) - val isAddressInWallet = isNotAddressInWallet( - address = value, - walletAddresses = walletAddressesProvider(), - ) + val isValidMemo = validateWalletMemoUseCase( + memo = recipientState.memoTextField?.value.orEmpty(), + network = cryptoCurrencyStatus.currency.network, + ).getOrElse { + Timber.e("Failed to validateWalletMemoUseCase: $it") + false + } + val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses + ?.any { it.value == value } ?: true + return state.copy( recipientState = recipientState.copy( - isPrimaryButtonEnabled = isValidMemo && isValidAddress && isAddressInWallet, + isPrimaryButtonEnabled = isValidMemo && isValidAddress && !isAddressInWallet, isValidating = false, addressTextField = recipientState.addressTextField.copy( error = when { - !isValidAddress || !isAddressInWallet -> resourceReference( + !isValidAddress || isAddressInWallet -> resourceReference( R.string.send_recipient_address_error, ) else -> null }, - isError = value.isNotEmpty() && !isValidAddress || !isAddressInWallet, + isError = value.isNotEmpty() && !isValidAddress || isAddressInWallet, ), ), ) @@ -185,23 +187,26 @@ internal class SendStateFactory( } fun getOnRecipientMemoValidState(value: String, isValidAddress: Boolean): SendUiState { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val state = currentStateProvider() val recipientState = state.recipientState ?: return state - val isValidMemo = validateMemo( + val isValidMemo = validateWalletMemoUseCase( memo = value, - cryptoCurrency = cryptoCurrencyStatusProvider().currency, - ) - val isAddressInWallet = isNotAddressInWallet( - walletAddresses = walletAddressesProvider(), - address = recipientState.addressTextField.value, - ) + network = cryptoCurrencyStatus.currency.network, + ).getOrElse { + Timber.e("Failed to validateWalletMemoUseCase: $it") + false + } + val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses + ?.any { it.value == value } ?: true + return state.copy( recipientState = recipientState.copy( - isPrimaryButtonEnabled = isValidMemo && isValidAddress && isAddressInWallet, + isPrimaryButtonEnabled = isValidMemo && isValidAddress && !isAddressInWallet, isValidating = false, memoTextField = recipientState.memoTextField?.copy( - isError = value.isNotEmpty() || isValidMemo && isValidAddress && isAddressInWallet, + isError = value.isNotEmpty() && !isValidMemo, ), ), ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index 2f17471513..d15db2208c 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -85,12 +85,10 @@ private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier ) } - val isButtonEnabled = remember { - isButtonEnabled( - currentState = currentState, - uiState = uiState, - ) - } + val isButtonEnabled = isButtonEnabled( + currentState = currentState, + uiState = uiState, + ) AnimatedContent( targetState = buttonTextId, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/AddressVerification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/AddressVerification.kt deleted file mode 100644 index 3dd89596a1..0000000000 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/AddressVerification.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.features.send.impl.presentation.viewmodel - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.address.Address -import com.tangem.domain.tokens.model.CryptoCurrency - -internal fun verifyAddress(address: String, cryptoCurrency: CryptoCurrency?): Boolean { - if (address.isEmpty()) return true - val blockchain = cryptoCurrency?.let { - Blockchain.fromId(cryptoCurrency.id.rawNetworkId) - } ?: return false - - return blockchain.validateAddress(address) -} - -internal fun isNotAddressInWallet(walletAddresses: Set
, address: String): Boolean { - return walletAddresses.all { it.value != address } -} \ No newline at end of file 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 8797df79a7..0b5cc51a05 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 @@ -15,7 +15,6 @@ import com.tangem.blockchain.blockchains.xrp.XrpAddressService import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.TransactionExtras -import com.tangem.blockchain.common.address.Address import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -38,6 +37,7 @@ import com.tangem.domain.wallets.models.UserWalletId 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.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.domain.AvailableWallet @@ -78,6 +78,7 @@ internal class SendViewModel @Inject constructor( private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val validateWalletAddressUseCase: ValidateWalletAddressUseCase, private val walletManagersFacade: WalletManagersFacade, + validateWalletMemoUseCase: ValidateWalletMemoUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, SendClickIntents { @@ -97,10 +98,10 @@ internal class SendViewModel @Inject constructor( clickIntents = this, currentStateProvider = Provider { uiState }, userWalletProvider = Provider { userWallet }, - walletAddressesProvider = Provider { walletAddresses }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, + validateWalletMemoUseCase = validateWalletMemoUseCase, ) var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState()) @@ -109,16 +110,13 @@ internal class SendViewModel @Inject constructor( private var userWallet: UserWallet by Delegates.notNull() private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() - private var walletAddresses = emptySet
() private var balanceJobHolder = JobHolder() private var recipientsJobHolder = JobHolder() - private var walletAddressesJobHolder = JobHolder() private var feeJobHolder = JobHolder() private var addressValidationJobHolder = JobHolder() override fun onCreate(owner: LifecycleOwner) { - getWalletAddresses() subscribeOnCurrencyStatusUpdates(owner) getFee() } @@ -310,15 +308,6 @@ internal class SendViewModel @Inject constructor( }.saveIn(feeJobHolder) } - private fun getWalletAddresses() { - viewModelScope.launch(dispatchers.io) { - walletAddresses = walletManagersFacade.getAddresses( - userWalletId = userWalletId, - network = cryptoCurrency.network, - ) - }.saveIn(walletAddressesJobHolder) - } - // region screen state navigation override fun popBackStack() = stateRouter.popBackStack() override fun onBackClick() = stateRouter.onBackClick() @@ -370,7 +359,7 @@ internal class SendViewModel @Inject constructor( viewModelScope.launch(dispatchers.main) { uiState = stateFactory.getOnRecipientAddressValidationStarted() if (!checkIfXrpAddressValue(value)) { - val isValidAddress = validateAddress(value) + val isValidAddress = validateAddress(uiState.recipientState?.addressTextField?.value.orEmpty()) uiState = stateFactory.getOnRecipientMemoValidState(value, isValidAddress) } }.saveIn(addressValidationJobHolder) @@ -508,7 +497,7 @@ internal class SendViewModel @Inject constructor( ) } } -// endregion + // endregion private fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? { val blockchain = Blockchain.fromId(networkId)