From 2e581a252fa0a48c78d1e788924c41239ec38084 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 13 Jul 2026 12:35:01 +0100 Subject: [PATCH] Updated on 2026-08-14 --- domain/address-book/build.gradle.kts | 4 ++ .../addressbook/usecase/GetContactsUseCase.kt | 6 ++- .../usecase/GetContactsUseCaseTest.kt | 45 ++++++++++++------- .../list/model/AddressBookListModel.kt | 2 +- .../destination/model/SendDestinationModel.kt | 10 ++++- .../model/SendDestinationModelTest.kt | 33 ++++++++++++++ 6 files changed, 81 insertions(+), 19 deletions(-) diff --git a/domain/address-book/build.gradle.kts b/domain/address-book/build.gradle.kts index a422d47b73..79b86b8be8 100644 --- a/domain/address-book/build.gradle.kts +++ b/domain/address-book/build.gradle.kts @@ -25,6 +25,10 @@ dependencies { implementation(projects.core.utils) // endregion + // region Libs + implementation(projects.libs.crypto) + // endregion + // region Domain api(projects.domain.common) api(projects.domain.tokens) diff --git a/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/usecase/GetContactsUseCase.kt b/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/usecase/GetContactsUseCase.kt index 6cae1951be..e85ba5940b 100644 --- a/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/usecase/GetContactsUseCase.kt +++ b/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/usecase/GetContactsUseCase.kt @@ -3,6 +3,7 @@ package com.tangem.domain.addressbook.usecase import com.tangem.domain.addressbook.model.Contact import com.tangem.domain.addressbook.repository.AddressBookRepository import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.lib.crypto.BlockchainUtils import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -30,7 +31,10 @@ class GetContactsUseCase( private fun Contact.matches(query: String): Boolean { val isNameContaining = name.value.contains(other = query, ignoreCase = true) val isAddressContaining = addresses.any { addressEntry -> - addressEntry.address.contains(other = query, ignoreCase = false) + val isCaseInsensitiveContractAddress = BlockchainUtils.isCaseInsensitiveContractAddress( + networkId = addressEntry.networkId.value, + ) + addressEntry.address.contains(other = query, ignoreCase = isCaseInsensitiveContractAddress) } val isNetworkContaining = addresses.any { addressEntry -> addressEntry.networkId.value.contains(other = query, ignoreCase = true) diff --git a/domain/address-book/src/test/kotlin/com/tangem/domain/addressbook/usecase/GetContactsUseCaseTest.kt b/domain/address-book/src/test/kotlin/com/tangem/domain/addressbook/usecase/GetContactsUseCaseTest.kt index fa60b3c50d..d5a26d4a10 100644 --- a/domain/address-book/src/test/kotlin/com/tangem/domain/addressbook/usecase/GetContactsUseCaseTest.kt +++ b/domain/address-book/src/test/kotlin/com/tangem/domain/addressbook/usecase/GetContactsUseCaseTest.kt @@ -54,31 +54,44 @@ class GetContactsUseCaseTest { } @Test - fun `GIVEN address query matching case WHEN invoke THEN returns matching contact`() = runTest { - // Arrange - val carol = contact(name = "Carol", address = "0xAbCdEf") - every { repository.getAllContacts() } returns flowOf(listOf(alice, carol)) - - // Act - val result = useCase(query = "0xAbCdEf").first() - - // Assert - assertThat(result).containsExactly(carol) - } - - @Test - fun `GIVEN address query with different case WHEN invoke THEN returns empty`() = runTest { - // Arrange - val carol = contact(name = "Carol", address = "0xAbCdEf") + fun `GIVEN EVM address query differing only in case WHEN invoke THEN returns matching contact`() = runTest { + // Arrange — EVM (ethereum) addresses are case-insensitive, so a lowercased query matches a checksummed address + val carol = contact(name = "Carol", address = "0xAbCdEf", networkId = "ethereum") every { repository.getAllContacts() } returns flowOf(listOf(alice, carol)) // Act val result = useCase(query = "0xabcdef").first() + // Assert + assertThat(result).containsExactly(carol) + } + + @Test + fun `GIVEN non-EVM address query differing only in case WHEN invoke THEN returns empty`() = runTest { + // Arrange — non-EVM (solana) addresses are case-sensitive, so a differently-cased query must not match + val dave = contact(name = "Dave", address = "SoLAnaAddr", networkId = "solana") + every { repository.getAllContacts() } returns flowOf(listOf(alice, dave)) + + // Act + val result = useCase(query = "solanaaddr").first() + // Assert assertThat(result).isEmpty() } + @Test + fun `GIVEN non-EVM address query with exact case WHEN invoke THEN returns matching contact`() = runTest { + // Arrange + val dave = contact(name = "Dave", address = "SoLAnaAddr", networkId = "solana") + every { repository.getAllContacts() } returns flowOf(listOf(alice, dave)) + + // Act + val result = useCase(query = "SoLAnaAddr").first() + + // Assert + assertThat(result).containsExactly(dave) + } + @Test fun `GIVEN query matches a network id WHEN invoke THEN returns only contacts on that network`() = runTest { // Arrange diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/model/AddressBookListModel.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/model/AddressBookListModel.kt index c91002fbfa..7203095b1b 100644 --- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/model/AddressBookListModel.kt +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/model/AddressBookListModel.kt @@ -68,7 +68,7 @@ internal class AddressBookListModel @Inject constructor( .shareIn(modelScope, SharingStarted.Lazily, replay = 1) init { - modelScope.launch(context = dispatchers.default) { + modelScope.launch { syncAddressBooksUseCase() sendContactListScreenOpenedEvent() observeContacts() diff --git a/features/send/impl/src/main/java/com/tangem/features/send/subcomponents/destination/model/SendDestinationModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/subcomponents/destination/model/SendDestinationModel.kt index 9ebade81e7..74f6520ad9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/subcomponents/destination/model/SendDestinationModel.kt @@ -38,6 +38,7 @@ import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.features.addressbook.AddressBookFeatureToggles import com.tangem.features.addressbook.AddressBookSendAnalytics import com.tangem.features.addressbook.ContactSelectionListener import com.tangem.features.addressbook.MatchedContact @@ -90,6 +91,7 @@ internal class SendDestinationModel @Inject constructor( private val sendBackupProblemEmailUseCase: SendBackupProblemEmailUseCase, private val addressBookSendAnalytics: AddressBookSendAnalytics, private val syncAddressBooksUseCase: SyncAddressBooksUseCase, + private val addressBookFeatureToggles: AddressBookFeatureToggles, getContactsUseCase: GetContactsUseCase, contactSelectionListener: ContactSelectionListener, ) : Model(), SendDestinationClickIntents { @@ -141,7 +143,7 @@ internal class SendDestinationModel @Inject constructor( private val backupProblematicWalletCache = AtomicReference?>(null) init { - modelScope.launch(context = dispatchers.default) { syncAddressBooksUseCase() } + syncAddressBooksIfNeeded() subscribeOnQRScannerResult() initialState() resetContactOnEdit() @@ -289,6 +291,12 @@ internal class SendDestinationModel @Inject constructor( }.launchIn(modelScope) } + private fun syncAddressBooksIfNeeded() { + if (addressBookFeatureToggles.isAddressBookEnabled) { + modelScope.launch(context = dispatchers.default) { syncAddressBooksUseCase() } + } + } + private fun subscribeOnQRScannerResult() { listenToQrScanningUseCase(SourceType.SEND) .getOrElse { emptyFlow() } diff --git a/features/send/impl/src/test/java/com/tangem/features/send/subcomponents/destination/model/SendDestinationModelTest.kt b/features/send/impl/src/test/java/com/tangem/features/send/subcomponents/destination/model/SendDestinationModelTest.kt index 1e11a2c4b0..8c81544ec3 100644 --- a/features/send/impl/src/test/java/com/tangem/features/send/subcomponents/destination/model/SendDestinationModelTest.kt +++ b/features/send/impl/src/test/java/com/tangem/features/send/subcomponents/destination/model/SendDestinationModelTest.kt @@ -34,6 +34,7 @@ import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.features.addressbook.AddressBookFeatureToggles import com.tangem.features.addressbook.AddressBookSendAnalytics import com.tangem.features.addressbook.ContactSelectionListener import com.tangem.features.addressbook.MatchedContact @@ -95,6 +96,7 @@ internal class SendDestinationModelTest { private val sendBackupProblemEmailUseCase: SendBackupProblemEmailUseCase = mockk(relaxed = true) private val getContactsUseCase: GetContactsUseCase = mockk(relaxed = true) private val syncAddressBooksUseCase: SyncAddressBooksUseCase = mockk(relaxed = true) + private val addressBookFeatureToggles: AddressBookFeatureToggles = mockk(relaxed = true) private val contactSelectionListener: ContactSelectionListener = mockk(relaxed = true) private val addressBookSendAnalytics: AddressBookSendAnalytics = mockk(relaxed = true) private val callback: SendDestinationComponent.ModelCallback = mockk(relaxed = true) @@ -515,6 +517,36 @@ internal class SendDestinationModelTest { ) } + @Nested + inner class SyncAddressBooks { + + @Test + fun `GIVEN address book enabled WHEN model initialized THEN sync address books`() = runTest { + // Arrange + every { addressBookFeatureToggles.isAddressBookEnabled } returns true + + // Act + buildModel() + advanceUntilIdle() + + // Assert + coVerify(exactly = 1) { syncAddressBooksUseCase() } + } + + @Test + fun `GIVEN address book disabled WHEN model initialized THEN do NOT sync address books`() = runTest { + // Arrange + every { addressBookFeatureToggles.isAddressBookEnabled } returns false + + // Act + buildModel() + advanceUntilIdle() + + // Assert + coVerify(exactly = 0) { syncAddressBooksUseCase() } + } + } + // region fixtures private fun TestScope.buildModel( @@ -574,6 +606,7 @@ internal class SendDestinationModelTest { sendBackupProblemEmailUseCase = sendBackupProblemEmailUseCase, addressBookSendAnalytics = addressBookSendAnalytics, syncAddressBooksUseCase = syncAddressBooksUseCase, + addressBookFeatureToggles = addressBookFeatureToggles, getContactsUseCase = getContactsUseCase, contactSelectionListener = contactSelectionListener, )