diff --git a/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/interactor/GetVerifiedContactsInteractor.kt b/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/interactor/GetVerifiedContactsInteractor.kt index 189080f135..f577620409 100644 --- a/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/interactor/GetVerifiedContactsInteractor.kt +++ b/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/interactor/GetVerifiedContactsInteractor.kt @@ -1,6 +1,6 @@ package com.tangem.domain.addressbook.interactor -import com.tangem.domain.addressbook.model.VerifiedContact +import com.tangem.domain.addressbook.model.Contact import com.tangem.domain.addressbook.usecase.GetContactsUseCase import com.tangem.domain.addressbook.verification.ContactSignatureVerifier import com.tangem.domain.models.wallet.UserWalletId @@ -12,7 +12,7 @@ class GetVerifiedContactsInteractor( private val contactSignatureVerifier: ContactSignatureVerifier, ) { - fun getVerifiedContacts(query: String, userWalletId: UserWalletId? = null): Flow> { + fun getVerifiedContacts(query: String, userWalletId: UserWalletId? = null): Flow> { return getContacts(query, userWalletId).map { contacts -> contactSignatureVerifier.verifyContacts(contacts) } diff --git a/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/model/AddressEntriesVerification.kt b/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/model/AddressEntriesVerification.kt deleted file mode 100644 index 65ab2749e9..0000000000 --- a/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/model/AddressEntriesVerification.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.domain.addressbook.model - -/** - * Outcome of verifying a [Contact]'s [AddressEntry]s against the wallet that signed them. - * - * @property valid entries whose signature was produced by the wallet — these should be shown. - * @property invalid entries that failed verification (tampered, signed by another wallet, or carrying - * a missing/malformed signature) — these should be hidden. - */ -data class AddressEntriesVerification( - val valid: List, - val invalid: List, -) { - val areAllInvalid: Boolean get() = valid.isEmpty() && invalid.isNotEmpty() -} \ No newline at end of file diff --git a/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/model/VerifiedContact.kt b/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/model/VerifiedContact.kt deleted file mode 100644 index 4568d5d3d7..0000000000 --- a/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/model/VerifiedContact.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.domain.addressbook.model - -/** - * @property contact the contact carrying only the entries whose signatures verified against the - * wallet — what should be shown to the user. - * @property invalidEntries entries that failed verification (tampered, signed by another wallet, or - * malformed). Hidden from the UI but kept for analytics. - */ -data class VerifiedContact( - val contact: Contact, - val invalidEntries: List, -) \ No newline at end of file diff --git a/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/usecase/CheckAddressDuplicateUseCase.kt b/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/usecase/CheckAddressDuplicateUseCase.kt index ba9494ac5e..ebb59c06df 100644 --- a/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/usecase/CheckAddressDuplicateUseCase.kt +++ b/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/usecase/CheckAddressDuplicateUseCase.kt @@ -30,7 +30,6 @@ class CheckAddressDuplicateUseCase( ): String? { val contacts = repository.getContactsSync(userWalletId) return contactSignatureVerifier.verifyContacts(contacts) - .map { it.contact } .firstOrNull { contact -> contact.id != excludeContactId && contact.addresses.any { entry -> entry.networkId.value == networkId && entry.address == address diff --git a/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/usecase/GetContactByIdUseCase.kt b/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/usecase/GetContactByIdUseCase.kt index 085f3654d1..ff644cc49e 100644 --- a/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/usecase/GetContactByIdUseCase.kt +++ b/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/usecase/GetContactByIdUseCase.kt @@ -20,9 +20,7 @@ class GetContactByIdUseCase( operator fun invoke(id: ContactId): Flow { return repository.getAllContacts().map { contacts -> val contact = contacts.find { it.id == id } ?: return@map null - contactSignatureVerifier.verifyContacts(listOf(contact)) - .firstOrNull() - ?.contact + contactSignatureVerifier.verifyContacts(listOf(contact)).firstOrNull() } } } \ No newline at end of file diff --git a/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/verification/ContactSignatureVerifier.kt b/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/verification/ContactSignatureVerifier.kt index b03872db97..919fe011ac 100644 --- a/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/verification/ContactSignatureVerifier.kt +++ b/domain/address-book/src/main/kotlin/com/tangem/domain/addressbook/verification/ContactSignatureVerifier.kt @@ -2,9 +2,8 @@ package com.tangem.domain.addressbook.verification import arrow.core.Either import arrow.core.right -import com.tangem.domain.addressbook.model.AddressEntriesVerification +import com.tangem.domain.addressbook.model.AddressEntry import com.tangem.domain.addressbook.model.Contact -import com.tangem.domain.addressbook.model.VerifiedContact import com.tangem.domain.addressbook.usecase.buildAddressEntryPayload import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet @@ -17,34 +16,24 @@ class ContactSignatureVerifier( private val userWalletsListRepository: UserWalletsListRepository, ) { - suspend fun verifyContacts(contacts: List): List { + suspend fun verifyContacts(contacts: List): List { val walletsById = userWalletsListRepository.userWalletsSync().associateBy { it.walletId } - return contacts - .mapNotNull { contact -> - val userWallet = walletsById[contact.walletId] ?: return@mapNotNull null - val verification = verify(userWallet, contact).getOrNull() ?: return@mapNotNull null - VerifiedContact( - contact = contact.copy(addresses = verification.valid), - invalidEntries = verification.invalid, - ) - } - .filter { verifiedContact -> - verifiedContact.contact.addresses.isNotEmpty() - } + return contacts.mapNotNull { contact -> + val userWallet = walletsById[contact.walletId] ?: return@mapNotNull null + val validEntries = verify(userWallet, contact).getOrNull() ?: return@mapNotNull null + contact.copy(addresses = validEntries).takeIf { validEntries.isNotEmpty() } + } } suspend fun isNameVerified(contact: Contact): Boolean { val userWallet = userWalletsListRepository.userWalletsSync() .firstOrNull { it.walletId == contact.walletId } ?: return false - return verify(userWallet, contact).getOrNull()?.valid?.isNotEmpty() == true + return verify(userWallet, contact).getOrNull()?.isNotEmpty() == true } - private fun verify( - userWallet: UserWallet, - contact: Contact, - ): Either { + private fun verify(userWallet: UserWallet, contact: Contact): Either> { val entries = contact.addresses - if (entries.isEmpty()) return AddressEntriesVerification(valid = emptyList(), invalid = emptyList()).right() + if (entries.isEmpty()) return emptyList().right() // Entries with a malformed (non-hex) signature can't be verified — they are invalid by format. val wellFormed = entries.mapNotNull { entry -> @@ -59,10 +48,7 @@ class ContactSignatureVerifier( .filterIndexed { index, _ -> flags[index] } .mapTo(HashSet()) { (entry, _) -> entry.id } - AddressEntriesVerification( - valid = entries.filter { it.id in validIds }, - invalid = entries.filterNot { it.id in validIds }, - ) + entries.filter { it.id in validIds } } } } \ No newline at end of file diff --git a/domain/address-book/src/test/kotlin/com/tangem/domain/addressbook/interactor/GetVerifiedContactsInteractorTest.kt b/domain/address-book/src/test/kotlin/com/tangem/domain/addressbook/interactor/GetVerifiedContactsInteractorTest.kt index 9965ba099e..b221101c8d 100644 --- a/domain/address-book/src/test/kotlin/com/tangem/domain/addressbook/interactor/GetVerifiedContactsInteractorTest.kt +++ b/domain/address-book/src/test/kotlin/com/tangem/domain/addressbook/interactor/GetVerifiedContactsInteractorTest.kt @@ -6,7 +6,6 @@ import com.tangem.domain.addressbook.model.AddressEntryId import com.tangem.domain.addressbook.model.Contact import com.tangem.domain.addressbook.model.ContactId import com.tangem.domain.addressbook.model.ContactName -import com.tangem.domain.addressbook.model.VerifiedContact import com.tangem.domain.addressbook.usecase.GetContactsUseCase import com.tangem.domain.addressbook.verification.ContactSignatureVerifier import com.tangem.domain.models.network.Network @@ -45,7 +44,7 @@ class GetVerifiedContactsInteractorTest { fun `GIVEN contacts WHEN getVerifiedContacts THEN maps them through the verifier`() = runTest { // Arrange val contact = contact() - val verified = VerifiedContact(contact = contact, invalidEntries = emptyList()) + val verified = contact.copy(addresses = emptyList()) every { getContacts(query = "query", userWalletId = walletId) } returns flowOf(listOf(contact)) coEvery { contactSignatureVerifier.verifyContacts(listOf(contact)) } returns listOf(verified) diff --git a/domain/address-book/src/test/kotlin/com/tangem/domain/addressbook/usecase/CheckAddressDuplicateUseCaseTest.kt b/domain/address-book/src/test/kotlin/com/tangem/domain/addressbook/usecase/CheckAddressDuplicateUseCaseTest.kt index 4feaffe28d..20d41bd547 100644 --- a/domain/address-book/src/test/kotlin/com/tangem/domain/addressbook/usecase/CheckAddressDuplicateUseCaseTest.kt +++ b/domain/address-book/src/test/kotlin/com/tangem/domain/addressbook/usecase/CheckAddressDuplicateUseCaseTest.kt @@ -27,9 +27,7 @@ class CheckAddressDuplicateUseCaseTest { fun resetMocks() { clearMocks(repository, contactSignatureVerifier) // Default: every stored address verifies, so the use case sees the contacts unchanged. - coEvery { contactSignatureVerifier.verifyContacts(any()) } answers { - firstArg>().map { VerifiedContact(contact = it, invalidEntries = emptyList()) } - } + coEvery { contactSignatureVerifier.verifyContacts(any()) } answers { firstArg>() } } @Test diff --git a/domain/address-book/src/test/kotlin/com/tangem/domain/addressbook/usecase/GetContactByIdUseCaseTest.kt b/domain/address-book/src/test/kotlin/com/tangem/domain/addressbook/usecase/GetContactByIdUseCaseTest.kt index 888e995a93..1d8a28dac7 100644 --- a/domain/address-book/src/test/kotlin/com/tangem/domain/addressbook/usecase/GetContactByIdUseCaseTest.kt +++ b/domain/address-book/src/test/kotlin/com/tangem/domain/addressbook/usecase/GetContactByIdUseCaseTest.kt @@ -6,7 +6,6 @@ import com.tangem.domain.addressbook.model.AddressEntryId import com.tangem.domain.addressbook.model.Contact import com.tangem.domain.addressbook.model.ContactId import com.tangem.domain.addressbook.model.ContactName -import com.tangem.domain.addressbook.model.VerifiedContact import com.tangem.domain.addressbook.repository.AddressBookRepository import com.tangem.domain.addressbook.verification.ContactSignatureVerifier import com.tangem.domain.models.network.Network @@ -43,8 +42,7 @@ class GetContactByIdUseCaseTest { val stored = contact("id-2", "Bob", valid, invalid) val verified = stored.copy(addresses = listOf(valid)) every { repository.getAllContacts() } returns flowOf(listOf(contact("id-1", "Alice"), stored)) - coEvery { contactSignatureVerifier.verifyContacts(listOf(stored)) } returns - listOf(VerifiedContact(contact = verified, invalidEntries = listOf(invalid))) + coEvery { contactSignatureVerifier.verifyContacts(listOf(stored)) } returns listOf(verified) // Act val result = useCase(ContactId("id-2")).first() diff --git a/domain/address-book/src/test/kotlin/com/tangem/domain/addressbook/verification/ContactSignatureVerifierTest.kt b/domain/address-book/src/test/kotlin/com/tangem/domain/addressbook/verification/ContactSignatureVerifierTest.kt index c7f9b05a90..8d3f9e5384 100644 --- a/domain/address-book/src/test/kotlin/com/tangem/domain/addressbook/verification/ContactSignatureVerifierTest.kt +++ b/domain/address-book/src/test/kotlin/com/tangem/domain/addressbook/verification/ContactSignatureVerifierTest.kt @@ -8,7 +8,6 @@ import com.tangem.domain.addressbook.model.AddressEntryId import com.tangem.domain.addressbook.model.Contact import com.tangem.domain.addressbook.model.ContactId import com.tangem.domain.addressbook.model.ContactName -import com.tangem.domain.addressbook.model.VerifiedContact import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet @@ -53,7 +52,7 @@ class ContactSignatureVerifierTest { inner class VerifyContacts { @Test - fun `GIVEN mixed entries WHEN verifyContacts THEN displays only valid AND keeps invalid for analytics`() = + fun `GIVEN mixed entries WHEN verifyContacts THEN keeps only the valid ones`() = runTest { // Arrange val valid = entry(id = "valid", address = "0xvalid", memo = null, signature = "AABB") @@ -65,12 +64,7 @@ class ContactSignatureVerifierTest { val result = verifier.verifyContacts(listOf(contact)) // Assert - assertThat(result).containsExactly( - VerifiedContact( - contact = contact.copy(addresses = listOf(valid)), - invalidEntries = listOf(invalid), - ), - ) + assertThat(result).containsExactly(contact.copy(addresses = listOf(valid))) } @Test @@ -101,7 +95,7 @@ class ContactSignatureVerifierTest { } @Test - fun `GIVEN some entries fail verification WHEN verifyContacts THEN partitions them preserving order`() = + fun `GIVEN some entries fail verification WHEN verifyContacts THEN keeps valid ones preserving order`() = runTest { // Arrange val valid1 = entry(id = "addr-1", address = "0xabc", memo = null, signature = "AABB") @@ -114,8 +108,7 @@ class ContactSignatureVerifierTest { val result = verifier.verifyContacts(listOf(contact)).single() // Assert - assertThat(result.contact.addresses).containsExactly(valid1, valid2).inOrder() - assertThat(result.invalidEntries).containsExactly(invalid) + assertThat(result.addresses).containsExactly(valid1, valid2).inOrder() } @Test @@ -135,8 +128,7 @@ class ContactSignatureVerifierTest { // Assert assertThat(signaturesSlot.captured.map { it.toHexString() }).containsExactly("AABB") - assertThat(result.contact.addresses).containsExactly(signed) - assertThat(result.invalidEntries).containsExactly(malformed) + assertThat(result.addresses).containsExactly(signed) } @Test @@ -199,12 +191,7 @@ class ContactSignatureVerifierTest { val result = verifier.verifyContacts(listOf(droppedContact, keptContact)) // Assert - assertThat(result).containsExactly( - VerifiedContact( - contact = keptContact.copy(addresses = listOf(validEntry)), - invalidEntries = emptyList(), - ), - ) + assertThat(result).containsExactly(keptContact.copy(addresses = listOf(validEntry))) } @Test diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/block/model/ContactsBlockModel.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/block/model/ContactsBlockModel.kt index 8b4a3c1704..5088f9a804 100644 --- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/block/model/ContactsBlockModel.kt +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/block/model/ContactsBlockModel.kt @@ -42,7 +42,6 @@ internal class ContactsBlockModel @Inject constructor( combine( params.queryFlow.flatMapLatest { query -> getVerifiedContactsInteractor.getVerifiedContacts(query = query, userWalletId = null) - .map { verified -> verified.map { it.contact } } }, getWalletsUseCase.invokeAsMap(isOnlyMultiCurrency = false, filterLocked = true), ) { contacts, wallets -> contacts to wallets.values.toList() } 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 7203095b1b..1f055fe4a5 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 @@ -8,7 +8,7 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.domain.addressbook.interactor.GetVerifiedContactsInteractor -import com.tangem.domain.addressbook.model.VerifiedContact +import com.tangem.domain.addressbook.model.Contact import com.tangem.domain.addressbook.usecase.SyncAddressBooksUseCase import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -63,7 +63,7 @@ internal class AddressBookListModel @Inject constructor( private val searchActive = MutableStateFlow(value = false) private val selectedWalletId = MutableStateFlow(value = null) - private val allContacts: SharedFlow> = + private val allContacts: SharedFlow> = getVerifiedContactsInteractor.getVerifiedContacts(query = "", userWalletId = null) .shareIn(modelScope, SharingStarted.Lazily, replay = 1) @@ -179,8 +179,8 @@ internal class AddressBookListModel @Inject constructor( } private data class ListInputs( - val allContacts: List, - val matchedContacts: List, + val allContacts: List, + val matchedContacts: List, val query: String, val selectedWalletId: String?, val wallets: Map, diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/state/transformers/UpdateAddressBookListContentTransformer.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/state/transformers/UpdateAddressBookListContentTransformer.kt index 908725f5b9..8e1c3ebeaa 100644 --- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/state/transformers/UpdateAddressBookListContentTransformer.kt +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/state/transformers/UpdateAddressBookListContentTransformer.kt @@ -4,7 +4,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.ds2.search.TangemSearch import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.addressbook.model.VerifiedContact +import com.tangem.domain.addressbook.model.Contact import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.addressbook.MatchedContact @@ -23,8 +23,8 @@ import kotlinx.collections.immutable.toImmutableList @Suppress("LongParameterList") internal class UpdateAddressBookListContentTransformer( wallets: Map, - private val allContacts: List, - private val matchedContacts: List, + private val allContacts: List, + private val matchedContacts: List, private val mode: AddressBookRoute.ListMode, private val selectedWalletId: String?, private val query: String, @@ -73,14 +73,14 @@ internal class UpdateAddressBookListContentTransformer( DefaultContactConverter(onContactClick).convertList(matchedContacts) is AddressBookRoute.ListMode.Selector -> SelectorContactConverter(onPickContact) - .convertList(ContactMatcher.match(matchedContacts.map { it.contact }, mode.networkId)) + .convertList(ContactMatcher.match(matchedContacts, mode.networkId)) } /** Wallets that own at least one contact (respecting the network filter in selector mode) — drives chip visibility. */ private fun totalWalletIds(): Set = when (val mode = mode) { - AddressBookRoute.ListMode.Default -> allContacts.mapTo(mutableSetOf()) { it.contact.walletId.stringValue } + AddressBookRoute.ListMode.Default -> allContacts.mapTo(mutableSetOf()) { it.walletId.stringValue } is AddressBookRoute.ListMode.Selector -> - ContactMatcher.match(allContacts.map { it.contact }, mode.networkId).mapTo(mutableSetOf()) { it.walletId } + ContactMatcher.match(allContacts, mode.networkId).mapTo(mutableSetOf()) { it.walletId } } private fun contentMode(): ContentMode = when (mode) { diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/state/transformers/converter/DefaultContactConverter.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/state/transformers/converter/DefaultContactConverter.kt index c29e57df86..8379f2f2b5 100644 --- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/state/transformers/converter/DefaultContactConverter.kt +++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/state/transformers/converter/DefaultContactConverter.kt @@ -1,30 +1,29 @@ package com.tangem.features.addressbook.list.state.transformers.converter import com.tangem.common.ui.account.AccountIconUM -import com.tangem.domain.addressbook.model.VerifiedContact +import com.tangem.domain.addressbook.model.Contact import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.features.addressbook.list.ui.state.ContactUM import com.tangem.utils.converter.Converter internal class DefaultContactConverter( private val onContactClick: (String) -> Unit, -) : Converter { +) : Converter { - override fun convert(value: VerifiedContact): ContactUM { - val contact = value.contact - val name = contact.name.value + override fun convert(value: Contact): ContactUM { + val name = value.name.value return ContactUM( - id = contact.id.value, - walletId = contact.walletId.stringValue, + id = value.id.value, + walletId = value.walletId.stringValue, name = name, icon = AccountIconUM.CryptoPortfolio( - value = CryptoPortfolioIcon.Icon.entries.firstOrNull { it.name == contact.icon } + value = CryptoPortfolioIcon.Icon.entries.firstOrNull { it.name == value.icon } ?: CryptoPortfolioIcon.Icon.Letter, - color = CryptoPortfolioIcon.Color.entries.firstOrNull { it.name == contact.iconColor } + color = CryptoPortfolioIcon.Color.entries.firstOrNull { it.name == value.iconColor } ?: CryptoPortfolioIcon.Color.Azure, ), - networkAddressCount = contact.addresses.size, - onClick = { onContactClick(contact.id.value) }, + networkAddressCount = value.addresses.size, + onClick = { onContactClick(value.id.value) }, ) } } \ No newline at end of file diff --git a/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/block/model/ContactsBlockModelTest.kt b/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/block/model/ContactsBlockModelTest.kt index ab3bf01ef1..e2a7dc5e7e 100644 --- a/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/block/model/ContactsBlockModelTest.kt +++ b/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/block/model/ContactsBlockModelTest.kt @@ -7,7 +7,6 @@ import com.tangem.domain.addressbook.model.* import com.tangem.domain.addressbook.usecase.SyncAddressBooksUseCase import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.addressbook.AddressBookContactsBlockComponent @@ -48,7 +47,7 @@ internal class ContactsBlockModelTest { fun resetMocks() { clearMocks(getVerifiedContactsInteractor, getWalletsUseCase, analyticsSender) every { getWalletsUseCase.invokeAsMap(isOnlyMultiCurrency = false, filterLocked = true) } returns - flowOf(linkedMapOf()) + flowOf(linkedMapOf()) } @AfterEach @@ -61,7 +60,7 @@ internal class ContactsBlockModelTest { fun `GIVEN matching contacts WHEN block populated THEN SendFlowWidgetShown sent once`() = runTest { // Arrange every { getVerifiedContactsInteractor.getVerifiedContacts(query = any(), userWalletId = null) } returns - flowOf(listOf(verified(id = "1"), verified(id = "2"))) + flowOf(listOf(contact(id = "1"), contact(id = "2"))) // Act createModel(testScope = this) @@ -91,7 +90,7 @@ internal class ContactsBlockModelTest { // Arrange var clicked: MatchedContact? = null every { getVerifiedContactsInteractor.getVerifiedContacts(query = any(), userWalletId = null) } returns - flowOf(listOf(verified(id = "42"))) + flowOf(listOf(contact(id = "42"))) val model = createModel(testScope = this, onContactClick = { clicked = it }) advanceUntilIdle() @@ -103,9 +102,6 @@ internal class ContactsBlockModelTest { assertThat(clicked?.contactId).isEqualTo("42") } - private fun verified(id: String): VerifiedContact = - VerifiedContact(contact = contact(id = id), invalidEntries = emptyList()) - private fun contact(id: String): Contact = Contact( id = ContactId(id), walletId = UserWalletId("a"), diff --git a/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/list/model/AddressBookListModelTest.kt b/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/list/model/AddressBookListModelTest.kt index 53294d8d38..cb46cea181 100644 --- a/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/list/model/AddressBookListModelTest.kt +++ b/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/list/model/AddressBookListModelTest.kt @@ -88,7 +88,7 @@ internal class AddressBookListModelTest { val syncGate = CompletableDeferred() coEvery { syncAddressBooksUseCase() } coAnswers { syncGate.await(); Unit.right() } every { getVerifiedContactsInteractor.getVerifiedContacts(query = "", userWalletId = null) } returns - flowOf(listOf(verifiedContact(id = "1", name = "Alice"))) + flowOf(listOf(contact(id = "1", name = "Alice"))) // Act val model = createModel(testScope = this, mode = AddressBookRoute.ListMode.Default) @@ -109,7 +109,7 @@ internal class AddressBookListModelTest { fun `GIVEN default mode AND verified contacts WHEN created THEN content shown`() = runTest { // Arrange every { getVerifiedContactsInteractor.getVerifiedContacts(query = "", userWalletId = null) } returns - flowOf(listOf(verifiedContact(id = "1", name = "Alice"), verifiedContact(id = "2", name = "Bob"))) + flowOf(listOf(contact(id = "1", name = "Alice"), contact(id = "2", name = "Bob"))) // Act val model = createModel(testScope = this, mode = AddressBookRoute.ListMode.Default) @@ -139,7 +139,7 @@ internal class AddressBookListModelTest { // Arrange var clickedId: String? = null every { getVerifiedContactsInteractor.getVerifiedContacts(query = "", userWalletId = null) } returns - flowOf(listOf(verifiedContact(id = "42", name = "Alice"))) + flowOf(listOf(contact(id = "42", name = "Alice"))) val model = createModel( testScope = this, mode = AddressBookRoute.ListMode.Default, @@ -159,7 +159,7 @@ internal class AddressBookListModelTest { runTest { // Arrange every { getVerifiedContactsInteractor.getVerifiedContacts(query = "", userWalletId = null) } returns - flowOf(listOf(verifiedContact(id = "1", name = "Alice"), verifiedContact(id = "2", name = "Bob"))) + flowOf(listOf(contact(id = "1", name = "Alice"), contact(id = "2", name = "Bob"))) // Act createModel(testScope = this, mode = AddressBookRoute.ListMode.Default) @@ -190,7 +190,7 @@ internal class AddressBookListModelTest { fun `GIVEN selector mode WHEN contact picked THEN ContactSelectedInSend sent`() = runTest { // Arrange — a contact with a single ethereum address matches the selection network. every { getVerifiedContactsInteractor.getVerifiedContacts(query = "", userWalletId = null) } returns - flowOf(listOf(verifiedContact(id = "42", name = "Alice"))) + flowOf(listOf(contact(id = "42", name = "Alice"))) val model = createModel( testScope = this, mode = AddressBookRoute.ListMode.Selector(networkId = "ethereum"), @@ -205,26 +205,23 @@ internal class AddressBookListModelTest { verify(exactly = 1) { analyticsSender.sendContactSelectedInSend(contactId = "42", scope = any()) } } - private fun verifiedContact(id: String, name: String): VerifiedContact = VerifiedContact( - contact = Contact( - id = ContactId(id), - walletId = UserWalletId("a"), - name = ContactName(name).getOrNull()!!, - icon = "", - iconColor = CryptoPortfolioIcon.Color.Azure.name, - createdAt = TIMESTAMP, - updatedAt = TIMESTAMP, - addresses = listOf( - AddressEntry( - id = AddressEntryId("e-$id"), - address = "0xABC", - networkId = Network.RawID("ethereum"), - memo = null, - signature = "sig", - ), + private fun contact(id: String, name: String): Contact = Contact( + id = ContactId(id), + walletId = UserWalletId("a"), + name = ContactName(name).getOrNull()!!, + icon = "", + iconColor = CryptoPortfolioIcon.Color.Azure.name, + createdAt = TIMESTAMP, + updatedAt = TIMESTAMP, + addresses = listOf( + AddressEntry( + id = AddressEntryId("e-$id"), + address = "0xABC", + networkId = Network.RawID("ethereum"), + memo = null, + signature = "sig", ), ), - invalidEntries = emptyList(), ) private fun createModel( diff --git a/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/list/state/transformers/UpdateAddressBookListContentTransformerTest.kt b/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/list/state/transformers/UpdateAddressBookListContentTransformerTest.kt index 95c466ec60..714aebc984 100644 --- a/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/list/state/transformers/UpdateAddressBookListContentTransformerTest.kt +++ b/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/list/state/transformers/UpdateAddressBookListContentTransformerTest.kt @@ -23,7 +23,7 @@ internal class UpdateAddressBookListContentTransformerTest { @Test fun `GIVEN contacts in one wallet WHEN blank query THEN no chips`() { // Arrange - val all = listOf(verified(wallet1, "Alice"), verified(wallet1, "Bob")) + val all = listOf(contact(wallet1, "Alice"), contact(wallet1, "Bob")) // Act val result = transform(allContacts = all, matchedContacts = all) @@ -40,7 +40,7 @@ internal class UpdateAddressBookListContentTransformerTest { @Test fun `GIVEN contacts in two wallets WHEN blank query THEN All plus per-wallet chips with All selected`() { // Arrange - val all = listOf(verified(wallet1, "Alice"), verified(wallet2, "Bob")) + val all = listOf(contact(wallet1, "Alice"), contact(wallet2, "Bob")) // Act val result = transform(allContacts = all, matchedContacts = all) @@ -58,7 +58,7 @@ internal class UpdateAddressBookListContentTransformerTest { @Test fun `GIVEN two wallets WHEN a wallet chip selected THEN list filtered but chips unchanged`() { // Arrange - val all = listOf(verified(wallet1, "Alice"), verified(wallet2, "Bob")) + val all = listOf(contact(wallet1, "Alice"), contact(wallet2, "Bob")) // Act val result = transform(allContacts = all, matchedContacts = all, selectedWalletId = wallet2) @@ -75,8 +75,8 @@ internal class UpdateAddressBookListContentTransformerTest { @Test fun `GIVEN two wallets WHEN query narrows to one wallet THEN chips kept as All plus that wallet`() { // Arrange — the book spans two wallets, but the query matched only wallet1 in the domain - val all = listOf(verified(wallet1, "Antonio"), verified(wallet2, "Bob")) - val matched = listOf(verified(wallet1, "Antonio")) + val all = listOf(contact(wallet1, "Antonio"), contact(wallet2, "Bob")) + val matched = listOf(contact(wallet1, "Antonio")) // Act val result = transform(allContacts = all, matchedContacts = matched, query = "Anto") @@ -91,8 +91,8 @@ internal class UpdateAddressBookListContentTransformerTest { @Test fun `GIVEN selected wallet no longer matches query THEN falls back to All`() { // Arrange - val all = listOf(verified(wallet1, "Antonio"), verified(wallet2, "Bob")) - val matched = listOf(verified(wallet1, "Antonio")) + val all = listOf(contact(wallet1, "Antonio"), contact(wallet2, "Bob")) + val matched = listOf(contact(wallet1, "Antonio")) // Act — selected wallet2, but query matched only wallet1 val result = transform( @@ -120,7 +120,7 @@ internal class UpdateAddressBookListContentTransformerTest { @Test fun `GIVEN query matches nothing WHEN non-blank query THEN nothing found and chips hidden`() { // Arrange - val all = listOf(verified(wallet1, "Alice"), verified(wallet2, "Bob")) + val all = listOf(contact(wallet1, "Alice"), contact(wallet2, "Bob")) // Act val result = transform(allContacts = all, matchedContacts = emptyList(), query = "Zzz") @@ -133,8 +133,8 @@ internal class UpdateAddressBookListContentTransformerTest { } private fun transform( - allContacts: List, - matchedContacts: List, + allContacts: List, + matchedContacts: List, selectedWalletId: String? = null, query: String = "", ): AddressBookListUM = UpdateAddressBookListContentTransformer( @@ -156,25 +156,22 @@ internal class UpdateAddressBookListContentTransformerTest { private fun wallet(id: String, name: String): UserWallet = MockUserWalletFactory.create().copy(walletId = UserWalletId(stringValue = id), name = name) - private fun verified(walletId: String, name: String): VerifiedContact = VerifiedContact( - contact = Contact( - id = ContactId(name + walletId), - walletId = UserWalletId(stringValue = walletId), - name = requireNotNull(ContactName(name).getOrNull()) { "invalid test name" }, - icon = "", - iconColor = "Azure", - createdAt = "2026-06-10T14:30:00.000Z", - updatedAt = "2026-06-10T14:30:00.000Z", - addresses = listOf( - AddressEntry( - id = AddressEntryId(name), - address = "addr-$name", - networkId = Network.RawID("ethereum"), - memo = null, - signature = "sig", - ), + private fun contact(walletId: String, name: String): Contact = Contact( + id = ContactId(name + walletId), + walletId = UserWalletId(stringValue = walletId), + name = requireNotNull(ContactName(name).getOrNull()) { "invalid test name" }, + icon = "", + iconColor = "Azure", + createdAt = "2026-06-10T14:30:00.000Z", + updatedAt = "2026-06-10T14:30:00.000Z", + addresses = listOf( + AddressEntry( + id = AddressEntryId(name), + address = "addr-$name", + networkId = Network.RawID("ethereum"), + memo = null, + signature = "sig", ), ), - invalidEntries = emptyList(), ) } \ No newline at end of file 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 d8946bc0ce..fb2eeeaf48 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 @@ -102,7 +102,6 @@ internal class SendDestinationModel @Inject constructor( private val contacts: StateFlow> = getVerifiedContactsInteractor.getVerifiedContacts(query = "", userWalletId = null) - .map { verified -> verified.map { it.contact } } .flowOn(dispatchers.default) .stateIn(modelScope, SharingStarted.Eagerly, emptyList()) 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 358585e456..1a98a91a91 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 @@ -390,7 +390,7 @@ internal class SendDestinationModelTest { validateWalletAddressUseCase(any(), any(), any(), any>(), any()) } returns AddressValidation.Success.Valid.right() every { getVerifiedContactsInteractor.getVerifiedContacts(any(), any()) } returns - flowOf(listOf(verified(buildContact(name = model.savedName, address = model.savedAddress)))) + flowOf(listOf(buildContact(name = model.savedName, address = model.savedAddress))) val sut = buildModel() advanceUntilIdle() @@ -476,7 +476,7 @@ internal class SendDestinationModelTest { validateWalletAddressUseCase(any(), any(), any(), any>(), any()) } returns AddressValidation.Success.Valid.right() every { getVerifiedContactsInteractor.getVerifiedContacts(any(), any()) } returns - flowOf(model.savedAddresses.map { verified(buildContact(address = it)) }) + flowOf(model.savedAddresses.map { buildContact(address = it) }) val sut = buildBlockModel(isAddContactAvailable = model.isAddContactAvailable) advanceUntilIdle() @@ -608,9 +608,6 @@ internal class SendDestinationModelTest { ) } - private fun verified(contact: Contact): VerifiedContact = - VerifiedContact(contact = contact, invalidEntries = emptyList()) - private fun buildContact(name: String = "Alice", address: String = "0xAddr"): Contact = Contact( id = ContactId("c1"), walletId = testUserWalletId,