From eaf587e7b84b6af4860cee7f1d6e141d4cab8bd1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Jul 2025 12:07:19 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../DefaultWalletAddressServiceRepository.kt | 24 +++++++ .../WalletAddressServiceRepository.kt | 3 + .../transaction/error/ValidateAddressError.kt | 1 + .../usecase/ValidateWalletAddressUseCase.kt | 62 ++++++++++++------- .../send/v2/send/DefaultSendComponent.kt | 2 +- .../v2/send/confirm/model/SendConfirmModel.kt | 4 +- .../features/send/v2/send/model/SendModel.kt | 2 +- .../v2/sendnft/DefaultNFTSendComponent.kt | 2 +- .../confirm/model/NFTSendConfirmModel.kt | 4 +- .../send/v2/sendnft/model/NFTSendModel.kt | 2 +- ...dDestinationValidationResultTransformer.kt | 3 + .../ui/state/DestinationTextFieldUM.kt | 8 ++- 12 files changed, 85 insertions(+), 32 deletions(-) diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt index a85f05371d..067feb23d3 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt @@ -4,6 +4,8 @@ import android.net.Uri import androidx.core.text.isDigitsOnly import com.tangem.blockchain.blockchains.near.NearWalletManager import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.NameResolver +import com.tangem.blockchain.common.ResolveAddressResult import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.domain.models.network.Network import com.tangem.domain.transaction.WalletAddressServiceRepository @@ -20,6 +22,28 @@ class DefaultWalletAddressServiceRepository( private val dispatchers: CoroutineDispatcherProvider, ) : WalletAddressServiceRepository { + override suspend fun resolveAddress( + userWalletId: UserWalletId, + network: Network, + address: String, + ): ResolveAddressResult { + return withContext(dispatchers.io) { + val blockchain = network.toBlockchain() + + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + + if (walletManager is NameResolver) { + walletManager.resolve(address) + } else { + ResolveAddressResult.NotSupported + } + } + } + override suspend fun validateAddress(userWalletId: UserWalletId, network: Network, address: String): Boolean = withContext(dispatchers.io) { val blockchain = network.toBlockchain() diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt index 02e97c1741..048ecf0ab1 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt @@ -1,5 +1,6 @@ package com.tangem.domain.transaction +import com.tangem.blockchain.common.ResolveAddressResult import com.tangem.domain.models.network.Network import com.tangem.domain.wallets.models.ParsedQrCode import com.tangem.domain.wallets.models.UserWalletId @@ -9,6 +10,8 @@ import com.tangem.domain.wallets.models.UserWalletId */ interface WalletAddressServiceRepository { + suspend fun resolveAddress(userWalletId: UserWalletId, network: Network, address: String): ResolveAddressResult + suspend fun validateAddress(userWalletId: UserWalletId, network: Network, address: String): Boolean fun validateMemo(network: Network, memo: String): Boolean diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateAddressError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateAddressError.kt index 8fc2ee7870..f8aff8be0d 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateAddressError.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateAddressError.kt @@ -8,6 +8,7 @@ sealed class AddressValidation { sealed class Success : AddressValidation() { data object Valid : Success() data object ValidXAddress : Success() + data class ValidNamedAddress(val blockchainAddress: String) : Success() } sealed class Error : AddressValidation() { diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt index fda7a4282b..7963605ae2 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt @@ -2,6 +2,7 @@ package com.tangem.domain.transaction.usecase import arrow.core.left import arrow.core.right +import com.tangem.blockchain.common.ResolveAddressResult import com.tangem.domain.models.network.CryptoCurrencyAddress import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkAddress @@ -24,44 +25,59 @@ class ValidateWalletAddressUseCase( userWalletId: UserWalletId, network: Network, address: String, - currencyAddress: Set?, - ): AddressValidationResult { - val decodedXAddress = BlockchainUtils.decodeRippleXAddress(address, network.rawId) - val isUtxoConsolidationAvailable = - walletManagersFacade.checkUtxoConsolidationAvailability(userWalletId, network) - - val addressToValidate = decodedXAddress?.address ?: address - val isCurrentAddress = currencyAddress?.any { it.value == addressToValidate } ?: true - - val isForbidSelfSend = isCurrentAddress && !isUtxoConsolidationAvailable - val isValidAddress = walletAddressServiceRepository.validateAddress(userWalletId, network, addressToValidate) - - return when { - !isValidAddress -> AddressValidation.Error.InvalidAddress.left() - isForbidSelfSend -> AddressValidation.Error.AddressInWallet.left() - decodedXAddress != null -> AddressValidation.Success.ValidXAddress.right() - else -> AddressValidation.Success.Valid.right() - } - } + currencyAddresses: Set?, + ): AddressValidationResult = validateAddressInternal( + userWalletId, + network, + address, + isCurrentAddress = { toValidate -> + currencyAddresses?.any { it.value == toValidate } ?: true + }, + ) suspend operator fun invoke( userWalletId: UserWalletId, network: Network, address: String, senderAddresses: List, + ): AddressValidationResult = validateAddressInternal( + userWalletId, + network, + address, + isCurrentAddress = { toValidate -> + senderAddresses.any { it.address == toValidate } + }, + ) + + private suspend fun validateAddressInternal( + userWalletId: UserWalletId, + network: Network, + address: String, + isCurrentAddress: (String) -> Boolean, ): AddressValidationResult { val decodedXAddress = BlockchainUtils.decodeRippleXAddress(address, network.rawId) val isUtxoConsolidationAvailable = walletManagersFacade.checkUtxoConsolidationAvailability(userWalletId, network) val addressToValidate = decodedXAddress?.address ?: address - val isCurrentAddress = senderAddresses.any { it.address == addressToValidate } - - val isForbidSelfSend = isCurrentAddress && !isUtxoConsolidationAvailable + val current = isCurrentAddress(addressToValidate) + val isForbidSelfSend = current && !isUtxoConsolidationAvailable val isValidAddress = walletAddressServiceRepository.validateAddress(userWalletId, network, addressToValidate) return when { - !isValidAddress -> AddressValidation.Error.InvalidAddress.left() + !isValidAddress -> { + val resolveAddressResult = walletAddressServiceRepository.resolveAddress( + userWalletId = userWalletId, + network = network, + address = addressToValidate, + ) + + if (resolveAddressResult is ResolveAddressResult.Resolved) { + AddressValidation.Success.ValidNamedAddress(resolveAddressResult.address).right() + } else { + AddressValidation.Error.InvalidAddress.left() + } + } isForbidSelfSend -> AddressValidation.Error.AddressInWallet.left() decodedXAddress != null -> AddressValidation.Success.ValidXAddress.right() else -> AddressValidation.Success.Valid.right() diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index 4f091935b8..491fcedcb7 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -234,7 +234,7 @@ internal class DefaultSendComponent @AssistedInject constructor( private fun getFeeComponent(factoryContext: AppComponentContext): ComposableContentComponent { val state = model.uiState.value val sendAmount = (state.amountUM as? AmountState.Data)?.amountTextField?.cryptoAmount?.value - val destinationAddress = (state.destinationUM as? DestinationUM.Content)?.addressTextField?.value + val destinationAddress = (state.destinationUM as? DestinationUM.Content)?.addressTextField?.actualAddress val cryptoCurrencyStatus = model.cryptoCurrencyStatusFlow.value val feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatusFlow.value diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index d9d8e350dd..4110801b3d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -122,7 +122,7 @@ internal class SendConfirmModel @Inject constructor( enteredMemo = destinationUM?.memoTextField?.value, reduceAmountBy = amountState?.reduceAmountBy.orZero(), isIgnoreReduce = amountState?.isIgnoreReduce == true, - enteredDestination = destinationUM?.addressTextField?.value, + enteredDestination = destinationUM?.addressTextField?.actualAddress, fee = feeSelectorUM?.selectedFee, feeError = (feeUM?.feeSelectorUM as? FeeSelectorUM.Error)?.error, ) @@ -316,7 +316,7 @@ internal class SendConfirmModel @Inject constructor( private fun verifyAndSendTransaction() { val amountValue = amountState?.amountTextField?.cryptoAmount?.value ?: return - val destination = destinationUM?.addressTextField?.value ?: return + val destination = destinationUM?.addressTextField?.actualAddress ?: return val memo = destinationUM?.memoTextField?.value val fee = feeSelectorUM?.selectedFee val feeValue = fee?.amount?.value ?: return diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt index 2ea110f6a2..17d9046d23 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -165,7 +165,7 @@ internal class SendModel @Inject constructor( } else { val destinationUM = uiState.value.destinationUM as? DestinationUM.Content ?: error("Invalid destination") val amountUM = uiState.value.amountUM as? AmountState.Data ?: error("Invalid amount") - val enteredDestinationAddress = destinationUM.addressTextField.value + val enteredDestinationAddress = destinationUM.addressTextField.actualAddress val enteredMemo = destinationUM.memoTextField?.value val enteredAmount = amountUM.amountTextField.cryptoAmount.value ?: error("Invalid amount") diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt index fd86d873b6..2c8f31fd10 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt @@ -157,7 +157,7 @@ internal class DefaultNFTSendComponent @AssistedInject constructor( private fun getFeeComponent(factoryContext: AppComponentContext): ComposableContentComponent { val state = model.uiState.value - val destinationAddress = (state.destinationUM as? DestinationUM.Content)?.addressTextField?.value + val destinationAddress = (state.destinationUM as? DestinationUM.Content)?.addressTextField?.actualAddress return if (destinationAddress != null) { SendFeeComponent( appComponentContext = factoryContext, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt index e9121d6a4c..e0c6b167d7 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -108,7 +108,7 @@ internal class NFTSendConfirmModel @Inject constructor( val confirmData: ConfirmData get() = ConfirmData( - enteredDestination = destinationUM?.addressTextField?.value, + enteredDestination = destinationUM?.addressTextField?.actualAddress, enteredMemo = destinationUM?.memoTextField?.value, fee = feeSelectorUM?.selectedFee, feeError = (feeUM?.feeSelectorUM as? FeeSelectorUM.Error)?.error, @@ -258,7 +258,7 @@ internal class NFTSendConfirmModel @Inject constructor( } private fun verifyAndSendTransaction() { - val destination = destinationUM?.addressTextField?.value ?: return + val destination = destinationUM?.addressTextField?.actualAddress ?: return val memo = destinationUM?.memoTextField?.value val fee = feeSelectorUM?.selectedFee ?: return val ownerAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value ?: return diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt index 4bf63dbc36..52919729c3 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt @@ -112,7 +112,7 @@ internal class NFTSendModel @Inject constructor( val destinationUM = uiState.value.destinationUM as? DestinationUM.Content ?: error("Invalid destination") val ownerAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value ?: error("Invalid owner address") - val enteredDestinationAddress = destinationUM.addressTextField.value + val enteredDestinationAddress = destinationUM.addressTextField.actualAddress val enteredMemo = destinationUM.memoTextField?.value val nftAsset = NFTSdkAssetConverter.convertBack(params.nftAsset).second diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt index a67c26d520..b5a3177c5f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt @@ -31,6 +31,8 @@ internal class SendDestinationValidationResultTransformer( }.leftOrNull() val shouldDisableMemo = shouldDisableMemo() + val blockchainAddress = + (addressValidationResult.getOrNull() as? AddressValidation.Success.ValidNamedAddress)?.blockchainAddress return state.copy( isValidating = false, @@ -38,6 +40,7 @@ internal class SendDestinationValidationResultTransformer( addressTextField = state.addressTextField.copy( error = addressErrorText?.let(::resourceReference), isError = state.addressTextField.value.isNotEmpty() && !isValidAddress, + blockchainAddress = blockchainAddress, ), memoTextField = state.memoTextField?.copy( value = state.memoTextField.value.takeIf { !shouldDisableMemo }.orEmpty(), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationTextFieldUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationTextFieldUM.kt index ccc6266ae2..abf90f46a7 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationTextFieldUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationTextFieldUM.kt @@ -21,7 +21,13 @@ internal sealed class DestinationTextFieldUM { val isError: Boolean = false, val error: TextReference? = null, val isValuePasted: Boolean, - ) : DestinationTextFieldUM() + // if value is human-readable address, this field contains the actual blockchain address + val blockchainAddress: String? = null, + ) : DestinationTextFieldUM() { + + val actualAddress: String + get() = blockchainAddress ?: value + } data class RecipientMemo( override val value: String,