Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-01 12:07:19 +03:00
parent 8aa6ce0100
commit eaf587e7b8
12 changed files with 85 additions and 32 deletions

View file

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

View file

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

View file

@ -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() {

View file

@ -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<NetworkAddress.Address>?,
): 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<NetworkAddress.Address>?,
): 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<CryptoCurrencyAddress>,
): 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()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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