From bb2877e36caebb9418b03b96801cbdaa6568c63c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 2 Apr 2026 11:22:06 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../usecase/ValidateWalletAddressUseCase.kt | 19 +- .../ValidateWalletAddressUseCaseTest.kt | 207 ++++++++++++++++++ .../SendDestinationComponentParams.kt | 3 + .../destination/model/SendDestinationModel.kt | 1 + .../DefaultSendWithSwapComponent.kt | 1 + .../confirm/SendWithSwapConfirmComponent.kt | 1 + 6 files changed, 225 insertions(+), 7 deletions(-) create mode 100644 domain/transaction/src/test/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCaseTest.kt 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 88413f9891..ab8e62b026 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 @@ -26,10 +26,12 @@ class ValidateWalletAddressUseCase( network: Network, address: String, currencyAddresses: Set?, + allowSelfSend: Boolean = false, ): AddressValidationResult = validateAddressInternal( - userWalletId, - network, - address, + userWalletId = userWalletId, + network = network, + address = address, + allowSelfSend = allowSelfSend, isCurrentAddress = { toValidate -> currencyAddresses?.any { it.value == toValidate } ?: true }, @@ -40,10 +42,12 @@ class ValidateWalletAddressUseCase( network: Network, address: String, senderAddresses: List, + allowSelfSend: Boolean = false, ): AddressValidationResult = validateAddressInternal( - userWalletId, - network, - address, + userWalletId = userWalletId, + network = network, + address = address, + allowSelfSend = allowSelfSend, isCurrentAddress = { toValidate -> senderAddresses.any { it.address == toValidate } }, @@ -53,6 +57,7 @@ class ValidateWalletAddressUseCase( userWalletId: UserWalletId, network: Network, address: String, + allowSelfSend: Boolean, isCurrentAddress: (String) -> Boolean, ): AddressValidationResult { val decodedXAddress = BlockchainUtils.decodeRippleXAddress(address, network.rawId) @@ -60,7 +65,7 @@ class ValidateWalletAddressUseCase( val addressToValidate = decodedXAddress?.address ?: address val current = isCurrentAddress(addressToValidate) - val isForbidSelfSend = current && !isSelfSendAvailable + val isForbidSelfSend = current && !isSelfSendAvailable && !allowSelfSend val isValidAddress = walletAddressServiceRepository.validateAddress(userWalletId, network, addressToValidate) return when { diff --git a/domain/transaction/src/test/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCaseTest.kt b/domain/transaction/src/test/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCaseTest.kt new file mode 100644 index 0000000000..5e66d72fe9 --- /dev/null +++ b/domain/transaction/src/test/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCaseTest.kt @@ -0,0 +1,207 @@ +package com.tangem.domain.transaction.usecase + +import com.google.common.truth.Truth.assertThat +import com.tangem.blockchain.common.ResolveAddressResult +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.CryptoCurrencyAddress +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.network.NetworkAddress +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.transaction.WalletAddressServiceRepository +import com.tangem.domain.transaction.error.AddressValidation +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.lib.crypto.models.XrpTaggedAddress +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkObject +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test + +internal class ValidateWalletAddressUseCaseTest { + + private val repository: WalletAddressServiceRepository = mockk() + private val walletManagersFacade: WalletManagersFacade = mockk() + private val useCase = ValidateWalletAddressUseCase( + walletAddressServiceRepository = repository, + walletManagersFacade = walletManagersFacade, + ) + + private val userWalletId: UserWalletId = mockk() + private val network: Network = mockk() + + @Before + fun setUp() { + mockkObject(BlockchainUtils) + every { BlockchainUtils.decodeRippleXAddress(any(), any()) } returns null + every { network.rawId } returns "ethereum" + } + + @After + fun tearDown() { + unmockkObject(BlockchainUtils) + } + + @Test + fun `GIVEN address not in sender list AND address is valid WHEN invoke THEN returns Valid`() = runTest { + val address = "0xRecipient" + val senderAddresses = listOf(senderAddress("0xSender")) + + coEvery { walletManagersFacade.checkSelfSendAvailability(userWalletId, network) } returns false + coEvery { repository.validateAddress(userWalletId, network, address) } returns true + + val result = useCase(userWalletId, network, address, senderAddresses) + + assertThat(result.getOrNull()).isEqualTo(AddressValidation.Success.Valid) + } + + @Test + fun `GIVEN address in sender list AND self-send not available WHEN invoke THEN returns AddressInWallet`() = runTest { + val address = "0xSender" + val senderAddresses = listOf(senderAddress(address)) + + coEvery { walletManagersFacade.checkSelfSendAvailability(userWalletId, network) } returns false + coEvery { repository.validateAddress(userWalletId, network, address) } returns true + + val result = useCase(userWalletId, network, address, senderAddresses) + + assertThat(result.leftOrNull()).isEqualTo(AddressValidation.Error.AddressInWallet) + } + + @Test + fun `GIVEN address in sender list AND self-send available WHEN invoke THEN returns Valid`() = runTest { + val address = "0xSender" + val senderAddresses = listOf(senderAddress(address)) + + coEvery { walletManagersFacade.checkSelfSendAvailability(userWalletId, network) } returns true + coEvery { repository.validateAddress(userWalletId, network, address) } returns true + + val result = useCase(userWalletId, network, address, senderAddresses) + + assertThat(result.getOrNull()).isEqualTo(AddressValidation.Success.Valid) + } + + @Test + fun `GIVEN address in sender list AND self-send not available AND allowSelfSend is true WHEN invoke THEN returns Valid`() = runTest { + val address = "0xSender" + val senderAddresses = listOf(senderAddress(address)) + + coEvery { walletManagersFacade.checkSelfSendAvailability(userWalletId, network) } returns false + coEvery { repository.validateAddress(userWalletId, network, address) } returns true + + val result = useCase(userWalletId, network, address, senderAddresses, allowSelfSend = true) + + assertThat(result.getOrNull()).isEqualTo(AddressValidation.Success.Valid) + } + + @Test + fun `GIVEN invalid address AND resolved as named address WHEN invoke THEN returns ValidNamedAddress`() = runTest { + val address = "vitalik.eth" + val resolvedAddress = "0xResolved" + val senderAddresses = listOf(senderAddress("0xSender")) + + coEvery { walletManagersFacade.checkSelfSendAvailability(userWalletId, network) } returns false + coEvery { repository.validateAddress(userWalletId, network, address) } returns false + coEvery { repository.resolveAddress(userWalletId, network, address) } returns + ResolveAddressResult.Resolved(resolvedAddress) + + val result = useCase(userWalletId, network, address, senderAddresses) + + assertThat(result.getOrNull()).isEqualTo(AddressValidation.Success.ValidNamedAddress(resolvedAddress)) + } + + @Test + fun `GIVEN invalid address AND not resolved WHEN invoke THEN returns InvalidAddress`() = runTest { + val address = "invalid_address" + val senderAddresses = listOf(senderAddress("0xSender")) + + coEvery { walletManagersFacade.checkSelfSendAvailability(userWalletId, network) } returns false + coEvery { repository.validateAddress(userWalletId, network, address) } returns false + coEvery { repository.resolveAddress(userWalletId, network, address) } returns + ResolveAddressResult.NotSupported + + val result = useCase(userWalletId, network, address, senderAddresses) + + assertThat(result.leftOrNull()).isEqualTo(AddressValidation.Error.InvalidAddress) + } + + @Test + fun `GIVEN valid XRP X-address WHEN invoke THEN returns ValidXAddress`() = runTest { + val xAddress = "X7AcgcsBL4L51nv2theWPZRMcGF37HeMBCFMDcaVEEF8Y3q" + val decodedAddress = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh" + val senderAddresses = listOf(senderAddress("0xSender")) + + every { BlockchainUtils.decodeRippleXAddress(xAddress, any()) } returns + XrpTaggedAddress(address = decodedAddress, destinationTag = null) + coEvery { walletManagersFacade.checkSelfSendAvailability(userWalletId, network) } returns false + coEvery { repository.validateAddress(userWalletId, network, decodedAddress) } returns true + + val result = useCase(userWalletId, network, xAddress, senderAddresses) + + assertThat(result.getOrNull()).isEqualTo(AddressValidation.Success.ValidXAddress) + } + + @Test + fun `GIVEN valid XRP X-address that decodes to sender address AND self-send not available WHEN invoke THEN returns AddressInWallet`() = runTest { + val xAddress = "X7AcgcsBL4L51nv2theWPZRMcGF37HeMBCFMDcaVEEF8Y3q" + val decodedAddress = "rSenderAddress" + val senderAddresses = listOf(senderAddress(decodedAddress)) + + every { BlockchainUtils.decodeRippleXAddress(xAddress, any()) } returns + XrpTaggedAddress(address = decodedAddress, destinationTag = null) + coEvery { walletManagersFacade.checkSelfSendAvailability(userWalletId, network) } returns false + coEvery { repository.validateAddress(userWalletId, network, decodedAddress) } returns true + + val result = useCase(userWalletId, network, xAddress, senderAddresses) + + assertThat(result.leftOrNull()).isEqualTo(AddressValidation.Error.AddressInWallet) + } + + @Test + fun `GIVEN null currencyAddresses AND self-send not available WHEN invoke THEN returns AddressInWallet`() = runTest { + val address = "0xSender" + + coEvery { walletManagersFacade.checkSelfSendAvailability(userWalletId, network) } returns false + coEvery { repository.validateAddress(userWalletId, network, address) } returns true + + val result = useCase(userWalletId, network, address, currencyAddresses = null) + + assertThat(result.leftOrNull()).isEqualTo(AddressValidation.Error.AddressInWallet) + } + + @Test + fun `GIVEN currencyAddresses not containing address AND address is valid WHEN invoke THEN returns Valid`() = runTest { + val address = "0xRecipient" + val currencyAddresses = setOf(networkAddress("0xSender")) + + coEvery { walletManagersFacade.checkSelfSendAvailability(userWalletId, network) } returns false + coEvery { repository.validateAddress(userWalletId, network, address) } returns true + + val result = useCase(userWalletId, network, address, currencyAddresses) + + assertThat(result.getOrNull()).isEqualTo(AddressValidation.Success.Valid) + } + + @Test + fun `GIVEN currencyAddresses containing address AND self-send not available AND allowSelfSend is true WHEN invoke THEN returns Valid`() = runTest { + val address = "0xSender" + val currencyAddresses = setOf(networkAddress(address)) + + coEvery { walletManagersFacade.checkSelfSendAvailability(userWalletId, network) } returns false + coEvery { repository.validateAddress(userWalletId, network, address) } returns true + + val result = useCase(userWalletId, network, address, currencyAddresses, allowSelfSend = true) + + assertThat(result.getOrNull()).isEqualTo(AddressValidation.Success.Valid) + } + + private fun senderAddress(address: String): CryptoCurrencyAddress = + CryptoCurrencyAddress(cryptoCurrency = mockk(relaxed = true), address = address) + + private fun networkAddress(value: String): NetworkAddress.Address = + NetworkAddress.Address(value = value, type = NetworkAddress.Address.Type.Primary) +} \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/SendDestinationComponentParams.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/SendDestinationComponentParams.kt index 267b5c896f..3c5613e62e 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/SendDestinationComponentParams.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/SendDestinationComponentParams.kt @@ -16,6 +16,7 @@ sealed class SendDestinationComponentParams { abstract val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource abstract val userWalletId: UserWalletId abstract val cryptoCurrency: CryptoCurrency + abstract val isAllowSelfSend: Boolean data class DestinationParams( override val state: DestinationUM, @@ -27,6 +28,7 @@ sealed class SendDestinationComponentParams { val isBalanceHidingFlow: StateFlow, val currentRoute: Flow, val callback: SendDestinationComponent.ModelCallback, + override val isAllowSelfSend: Boolean = false, ) : SendDestinationComponentParams() data class DestinationBlockParams( @@ -37,5 +39,6 @@ sealed class SendDestinationComponentParams { override val cryptoCurrency: CryptoCurrency, val blockClickEnableFlow: StateFlow, val predefinedValues: PredefinedValues, + override val isAllowSelfSend: Boolean = false, ) : SendDestinationComponentParams() } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index 6d844d1315..18ff42ff9e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -288,6 +288,7 @@ internal class SendDestinationModel @Inject constructor( network = cryptoCurrency.network, address = address, senderAddresses = senderAddresses.value, + allowSelfSend = params.isAllowSelfSend, ) val memoValidationResult = validateWalletMemoUseCase( userWalletId = userWalletId, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt index 9adf52cf8f..2a845f5472 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt @@ -188,6 +188,7 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( userWalletId = params.userWalletId, cryptoCurrency = secondaryCryptoCurrency, callback = model, + isAllowSelfSend = true, ), ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt index 1b837de7c9..d5728b155e 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt @@ -80,6 +80,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor( blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), cryptoCurrency = model.secondaryCurrency, predefinedValues = PredefinedValues.Empty, + isAllowSelfSend = true, ), onResult = model::onDestinationResult, onClick = model::showEditDestination,