Updated on 2026-08-14
This commit is contained in:
parent
6256f4ed23
commit
68297064be
19 changed files with 80 additions and 158 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
package com.tangem.domain.addressbook.interactor
|
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.usecase.GetContactsUseCase
|
||||||
import com.tangem.domain.addressbook.verification.ContactSignatureVerifier
|
import com.tangem.domain.addressbook.verification.ContactSignatureVerifier
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
|
@ -12,7 +12,7 @@ class GetVerifiedContactsInteractor(
|
||||||
private val contactSignatureVerifier: ContactSignatureVerifier,
|
private val contactSignatureVerifier: ContactSignatureVerifier,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun getVerifiedContacts(query: String, userWalletId: UserWalletId? = null): Flow<List<VerifiedContact>> {
|
fun getVerifiedContacts(query: String, userWalletId: UserWalletId? = null): Flow<List<Contact>> {
|
||||||
return getContacts(query, userWalletId).map { contacts ->
|
return getContacts(query, userWalletId).map { contacts ->
|
||||||
contactSignatureVerifier.verifyContacts(contacts)
|
contactSignatureVerifier.verifyContacts(contacts)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<AddressEntry>,
|
|
||||||
val invalid: List<AddressEntry>,
|
|
||||||
) {
|
|
||||||
val areAllInvalid: Boolean get() = valid.isEmpty() && invalid.isNotEmpty()
|
|
||||||
}
|
|
||||||
|
|
@ -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<AddressEntry>,
|
|
||||||
)
|
|
||||||
|
|
@ -30,7 +30,6 @@ class CheckAddressDuplicateUseCase(
|
||||||
): String? {
|
): String? {
|
||||||
val contacts = repository.getContactsSync(userWalletId)
|
val contacts = repository.getContactsSync(userWalletId)
|
||||||
return contactSignatureVerifier.verifyContacts(contacts)
|
return contactSignatureVerifier.verifyContacts(contacts)
|
||||||
.map { it.contact }
|
|
||||||
.firstOrNull { contact ->
|
.firstOrNull { contact ->
|
||||||
contact.id != excludeContactId && contact.addresses.any { entry ->
|
contact.id != excludeContactId && contact.addresses.any { entry ->
|
||||||
entry.networkId.value == networkId && entry.address == address
|
entry.networkId.value == networkId && entry.address == address
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,7 @@ class GetContactByIdUseCase(
|
||||||
operator fun invoke(id: ContactId): Flow<Contact?> {
|
operator fun invoke(id: ContactId): Flow<Contact?> {
|
||||||
return repository.getAllContacts().map { contacts ->
|
return repository.getAllContacts().map { contacts ->
|
||||||
val contact = contacts.find { it.id == id } ?: return@map null
|
val contact = contacts.find { it.id == id } ?: return@map null
|
||||||
contactSignatureVerifier.verifyContacts(listOf(contact))
|
contactSignatureVerifier.verifyContacts(listOf(contact)).firstOrNull()
|
||||||
.firstOrNull()
|
|
||||||
?.contact
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,9 +2,8 @@ package com.tangem.domain.addressbook.verification
|
||||||
|
|
||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
import arrow.core.right
|
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.Contact
|
||||||
import com.tangem.domain.addressbook.model.VerifiedContact
|
|
||||||
import com.tangem.domain.addressbook.usecase.buildAddressEntryPayload
|
import com.tangem.domain.addressbook.usecase.buildAddressEntryPayload
|
||||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
|
|
@ -17,34 +16,24 @@ class ContactSignatureVerifier(
|
||||||
private val userWalletsListRepository: UserWalletsListRepository,
|
private val userWalletsListRepository: UserWalletsListRepository,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun verifyContacts(contacts: List<Contact>): List<VerifiedContact> {
|
suspend fun verifyContacts(contacts: List<Contact>): List<Contact> {
|
||||||
val walletsById = userWalletsListRepository.userWalletsSync().associateBy { it.walletId }
|
val walletsById = userWalletsListRepository.userWalletsSync().associateBy { it.walletId }
|
||||||
return contacts
|
return contacts.mapNotNull { contact ->
|
||||||
.mapNotNull { contact ->
|
val userWallet = walletsById[contact.walletId] ?: return@mapNotNull null
|
||||||
val userWallet = walletsById[contact.walletId] ?: return@mapNotNull null
|
val validEntries = verify(userWallet, contact).getOrNull() ?: return@mapNotNull null
|
||||||
val verification = verify(userWallet, contact).getOrNull() ?: return@mapNotNull null
|
contact.copy(addresses = validEntries).takeIf { validEntries.isNotEmpty() }
|
||||||
VerifiedContact(
|
}
|
||||||
contact = contact.copy(addresses = verification.valid),
|
|
||||||
invalidEntries = verification.invalid,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.filter { verifiedContact ->
|
|
||||||
verifiedContact.contact.addresses.isNotEmpty()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun isNameVerified(contact: Contact): Boolean {
|
suspend fun isNameVerified(contact: Contact): Boolean {
|
||||||
val userWallet = userWalletsListRepository.userWalletsSync()
|
val userWallet = userWalletsListRepository.userWalletsSync()
|
||||||
.firstOrNull { it.walletId == contact.walletId } ?: return false
|
.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(
|
private fun verify(userWallet: UserWallet, contact: Contact): Either<VerifyMessagesError, List<AddressEntry>> {
|
||||||
userWallet: UserWallet,
|
|
||||||
contact: Contact,
|
|
||||||
): Either<VerifyMessagesError, AddressEntriesVerification> {
|
|
||||||
val entries = contact.addresses
|
val entries = contact.addresses
|
||||||
if (entries.isEmpty()) return AddressEntriesVerification(valid = emptyList(), invalid = emptyList()).right()
|
if (entries.isEmpty()) return emptyList<AddressEntry>().right()
|
||||||
|
|
||||||
// Entries with a malformed (non-hex) signature can't be verified — they are invalid by format.
|
// Entries with a malformed (non-hex) signature can't be verified — they are invalid by format.
|
||||||
val wellFormed = entries.mapNotNull { entry ->
|
val wellFormed = entries.mapNotNull { entry ->
|
||||||
|
|
@ -59,10 +48,7 @@ class ContactSignatureVerifier(
|
||||||
.filterIndexed { index, _ -> flags[index] }
|
.filterIndexed { index, _ -> flags[index] }
|
||||||
.mapTo(HashSet()) { (entry, _) -> entry.id }
|
.mapTo(HashSet()) { (entry, _) -> entry.id }
|
||||||
|
|
||||||
AddressEntriesVerification(
|
entries.filter { it.id in validIds }
|
||||||
valid = entries.filter { it.id in validIds },
|
|
||||||
invalid = entries.filterNot { it.id in validIds },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6,7 +6,6 @@ import com.tangem.domain.addressbook.model.AddressEntryId
|
||||||
import com.tangem.domain.addressbook.model.Contact
|
import com.tangem.domain.addressbook.model.Contact
|
||||||
import com.tangem.domain.addressbook.model.ContactId
|
import com.tangem.domain.addressbook.model.ContactId
|
||||||
import com.tangem.domain.addressbook.model.ContactName
|
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.usecase.GetContactsUseCase
|
||||||
import com.tangem.domain.addressbook.verification.ContactSignatureVerifier
|
import com.tangem.domain.addressbook.verification.ContactSignatureVerifier
|
||||||
import com.tangem.domain.models.network.Network
|
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 {
|
fun `GIVEN contacts WHEN getVerifiedContacts THEN maps them through the verifier`() = runTest {
|
||||||
// Arrange
|
// Arrange
|
||||||
val contact = contact()
|
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))
|
every { getContacts(query = "query", userWalletId = walletId) } returns flowOf(listOf(contact))
|
||||||
coEvery { contactSignatureVerifier.verifyContacts(listOf(contact)) } returns listOf(verified)
|
coEvery { contactSignatureVerifier.verifyContacts(listOf(contact)) } returns listOf(verified)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,7 @@ class CheckAddressDuplicateUseCaseTest {
|
||||||
fun resetMocks() {
|
fun resetMocks() {
|
||||||
clearMocks(repository, contactSignatureVerifier)
|
clearMocks(repository, contactSignatureVerifier)
|
||||||
// Default: every stored address verifies, so the use case sees the contacts unchanged.
|
// Default: every stored address verifies, so the use case sees the contacts unchanged.
|
||||||
coEvery { contactSignatureVerifier.verifyContacts(any()) } answers {
|
coEvery { contactSignatureVerifier.verifyContacts(any()) } answers { firstArg<List<Contact>>() }
|
||||||
firstArg<List<Contact>>().map { VerifiedContact(contact = it, invalidEntries = emptyList()) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import com.tangem.domain.addressbook.model.AddressEntryId
|
||||||
import com.tangem.domain.addressbook.model.Contact
|
import com.tangem.domain.addressbook.model.Contact
|
||||||
import com.tangem.domain.addressbook.model.ContactId
|
import com.tangem.domain.addressbook.model.ContactId
|
||||||
import com.tangem.domain.addressbook.model.ContactName
|
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.repository.AddressBookRepository
|
||||||
import com.tangem.domain.addressbook.verification.ContactSignatureVerifier
|
import com.tangem.domain.addressbook.verification.ContactSignatureVerifier
|
||||||
import com.tangem.domain.models.network.Network
|
import com.tangem.domain.models.network.Network
|
||||||
|
|
@ -43,8 +42,7 @@ class GetContactByIdUseCaseTest {
|
||||||
val stored = contact("id-2", "Bob", valid, invalid)
|
val stored = contact("id-2", "Bob", valid, invalid)
|
||||||
val verified = stored.copy(addresses = listOf(valid))
|
val verified = stored.copy(addresses = listOf(valid))
|
||||||
every { repository.getAllContacts() } returns flowOf(listOf(contact("id-1", "Alice"), stored))
|
every { repository.getAllContacts() } returns flowOf(listOf(contact("id-1", "Alice"), stored))
|
||||||
coEvery { contactSignatureVerifier.verifyContacts(listOf(stored)) } returns
|
coEvery { contactSignatureVerifier.verifyContacts(listOf(stored)) } returns listOf(verified)
|
||||||
listOf(VerifiedContact(contact = verified, invalidEntries = listOf(invalid)))
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
val result = useCase(ContactId("id-2")).first()
|
val result = useCase(ContactId("id-2")).first()
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import com.tangem.domain.addressbook.model.AddressEntryId
|
||||||
import com.tangem.domain.addressbook.model.Contact
|
import com.tangem.domain.addressbook.model.Contact
|
||||||
import com.tangem.domain.addressbook.model.ContactId
|
import com.tangem.domain.addressbook.model.ContactId
|
||||||
import com.tangem.domain.addressbook.model.ContactName
|
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.common.wallets.UserWalletsListRepository
|
||||||
import com.tangem.domain.models.network.Network
|
import com.tangem.domain.models.network.Network
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
|
|
@ -53,7 +52,7 @@ class ContactSignatureVerifierTest {
|
||||||
inner class VerifyContacts {
|
inner class VerifyContacts {
|
||||||
|
|
||||||
@Test
|
@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 {
|
runTest {
|
||||||
// Arrange
|
// Arrange
|
||||||
val valid = entry(id = "valid", address = "0xvalid", memo = null, signature = "AABB")
|
val valid = entry(id = "valid", address = "0xvalid", memo = null, signature = "AABB")
|
||||||
|
|
@ -65,12 +64,7 @@ class ContactSignatureVerifierTest {
|
||||||
val result = verifier.verifyContacts(listOf(contact))
|
val result = verifier.verifyContacts(listOf(contact))
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertThat(result).containsExactly(
|
assertThat(result).containsExactly(contact.copy(addresses = listOf(valid)))
|
||||||
VerifiedContact(
|
|
||||||
contact = contact.copy(addresses = listOf(valid)),
|
|
||||||
invalidEntries = listOf(invalid),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -101,7 +95,7 @@ class ContactSignatureVerifierTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 {
|
runTest {
|
||||||
// Arrange
|
// Arrange
|
||||||
val valid1 = entry(id = "addr-1", address = "0xabc", memo = null, signature = "AABB")
|
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()
|
val result = verifier.verifyContacts(listOf(contact)).single()
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertThat(result.contact.addresses).containsExactly(valid1, valid2).inOrder()
|
assertThat(result.addresses).containsExactly(valid1, valid2).inOrder()
|
||||||
assertThat(result.invalidEntries).containsExactly(invalid)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -135,8 +128,7 @@ class ContactSignatureVerifierTest {
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertThat(signaturesSlot.captured.map { it.toHexString() }).containsExactly("AABB")
|
assertThat(signaturesSlot.captured.map { it.toHexString() }).containsExactly("AABB")
|
||||||
assertThat(result.contact.addresses).containsExactly(signed)
|
assertThat(result.addresses).containsExactly(signed)
|
||||||
assertThat(result.invalidEntries).containsExactly(malformed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -199,12 +191,7 @@ class ContactSignatureVerifierTest {
|
||||||
val result = verifier.verifyContacts(listOf(droppedContact, keptContact))
|
val result = verifier.verifyContacts(listOf(droppedContact, keptContact))
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertThat(result).containsExactly(
|
assertThat(result).containsExactly(keptContact.copy(addresses = listOf(validEntry)))
|
||||||
VerifiedContact(
|
|
||||||
contact = keptContact.copy(addresses = listOf(validEntry)),
|
|
||||||
invalidEntries = emptyList(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ internal class ContactsBlockModel @Inject constructor(
|
||||||
combine(
|
combine(
|
||||||
params.queryFlow.flatMapLatest { query ->
|
params.queryFlow.flatMapLatest { query ->
|
||||||
getVerifiedContactsInteractor.getVerifiedContacts(query = query, userWalletId = null)
|
getVerifiedContactsInteractor.getVerifiedContacts(query = query, userWalletId = null)
|
||||||
.map { verified -> verified.map { it.contact } }
|
|
||||||
},
|
},
|
||||||
getWalletsUseCase.invokeAsMap(isOnlyMultiCurrency = false, filterLocked = true),
|
getWalletsUseCase.invokeAsMap(isOnlyMultiCurrency = false, filterLocked = true),
|
||||||
) { contacts, wallets -> contacts to wallets.values.toList() }
|
) { contacts, wallets -> contacts to wallets.values.toList() }
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import com.tangem.core.decompose.model.Model
|
||||||
import com.tangem.core.decompose.model.ParamsContainer
|
import com.tangem.core.decompose.model.ParamsContainer
|
||||||
import com.tangem.core.decompose.navigation.Router
|
import com.tangem.core.decompose.navigation.Router
|
||||||
import com.tangem.domain.addressbook.interactor.GetVerifiedContactsInteractor
|
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.addressbook.usecase.SyncAddressBooksUseCase
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
|
@ -63,7 +63,7 @@ internal class AddressBookListModel @Inject constructor(
|
||||||
private val searchActive = MutableStateFlow(value = false)
|
private val searchActive = MutableStateFlow(value = false)
|
||||||
private val selectedWalletId = MutableStateFlow<String?>(value = null)
|
private val selectedWalletId = MutableStateFlow<String?>(value = null)
|
||||||
|
|
||||||
private val allContacts: SharedFlow<List<VerifiedContact>> =
|
private val allContacts: SharedFlow<List<Contact>> =
|
||||||
getVerifiedContactsInteractor.getVerifiedContacts(query = "", userWalletId = null)
|
getVerifiedContactsInteractor.getVerifiedContacts(query = "", userWalletId = null)
|
||||||
.shareIn(modelScope, SharingStarted.Lazily, replay = 1)
|
.shareIn(modelScope, SharingStarted.Lazily, replay = 1)
|
||||||
|
|
||||||
|
|
@ -179,8 +179,8 @@ internal class AddressBookListModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class ListInputs(
|
private data class ListInputs(
|
||||||
val allContacts: List<VerifiedContact>,
|
val allContacts: List<Contact>,
|
||||||
val matchedContacts: List<VerifiedContact>,
|
val matchedContacts: List<Contact>,
|
||||||
val query: String,
|
val query: String,
|
||||||
val selectedWalletId: String?,
|
val selectedWalletId: String?,
|
||||||
val wallets: Map<UserWalletId, UserWallet>,
|
val wallets: Map<UserWalletId, UserWallet>,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import com.tangem.core.ui.R
|
||||||
import com.tangem.core.ui.ds2.search.TangemSearch
|
import com.tangem.core.ui.ds2.search.TangemSearch
|
||||||
import com.tangem.core.ui.extensions.resourceReference
|
import com.tangem.core.ui.extensions.resourceReference
|
||||||
import com.tangem.core.ui.extensions.stringReference
|
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.UserWallet
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.features.addressbook.MatchedContact
|
import com.tangem.features.addressbook.MatchedContact
|
||||||
|
|
@ -23,8 +23,8 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
internal class UpdateAddressBookListContentTransformer(
|
internal class UpdateAddressBookListContentTransformer(
|
||||||
wallets: Map<UserWalletId, UserWallet>,
|
wallets: Map<UserWalletId, UserWallet>,
|
||||||
private val allContacts: List<VerifiedContact>,
|
private val allContacts: List<Contact>,
|
||||||
private val matchedContacts: List<VerifiedContact>,
|
private val matchedContacts: List<Contact>,
|
||||||
private val mode: AddressBookRoute.ListMode,
|
private val mode: AddressBookRoute.ListMode,
|
||||||
private val selectedWalletId: String?,
|
private val selectedWalletId: String?,
|
||||||
private val query: String,
|
private val query: String,
|
||||||
|
|
@ -73,14 +73,14 @@ internal class UpdateAddressBookListContentTransformer(
|
||||||
DefaultContactConverter(onContactClick).convertList(matchedContacts)
|
DefaultContactConverter(onContactClick).convertList(matchedContacts)
|
||||||
is AddressBookRoute.ListMode.Selector ->
|
is AddressBookRoute.ListMode.Selector ->
|
||||||
SelectorContactConverter(onPickContact)
|
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. */
|
/** Wallets that own at least one contact (respecting the network filter in selector mode) — drives chip visibility. */
|
||||||
private fun totalWalletIds(): Set<String> = when (val mode = mode) {
|
private fun totalWalletIds(): Set<String> = 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 ->
|
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) {
|
private fun contentMode(): ContentMode = when (mode) {
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,29 @@
|
||||||
package com.tangem.features.addressbook.list.state.transformers.converter
|
package com.tangem.features.addressbook.list.state.transformers.converter
|
||||||
|
|
||||||
import com.tangem.common.ui.account.AccountIconUM
|
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.domain.models.account.CryptoPortfolioIcon
|
||||||
import com.tangem.features.addressbook.list.ui.state.ContactUM
|
import com.tangem.features.addressbook.list.ui.state.ContactUM
|
||||||
import com.tangem.utils.converter.Converter
|
import com.tangem.utils.converter.Converter
|
||||||
|
|
||||||
internal class DefaultContactConverter(
|
internal class DefaultContactConverter(
|
||||||
private val onContactClick: (String) -> Unit,
|
private val onContactClick: (String) -> Unit,
|
||||||
) : Converter<VerifiedContact, ContactUM> {
|
) : Converter<Contact, ContactUM> {
|
||||||
|
|
||||||
override fun convert(value: VerifiedContact): ContactUM {
|
override fun convert(value: Contact): ContactUM {
|
||||||
val contact = value.contact
|
val name = value.name.value
|
||||||
val name = contact.name.value
|
|
||||||
return ContactUM(
|
return ContactUM(
|
||||||
id = contact.id.value,
|
id = value.id.value,
|
||||||
walletId = contact.walletId.stringValue,
|
walletId = value.walletId.stringValue,
|
||||||
name = name,
|
name = name,
|
||||||
icon = AccountIconUM.CryptoPortfolio(
|
icon = AccountIconUM.CryptoPortfolio(
|
||||||
value = CryptoPortfolioIcon.Icon.entries.firstOrNull { it.name == contact.icon }
|
value = CryptoPortfolioIcon.Icon.entries.firstOrNull { it.name == value.icon }
|
||||||
?: CryptoPortfolioIcon.Icon.Letter,
|
?: CryptoPortfolioIcon.Icon.Letter,
|
||||||
color = CryptoPortfolioIcon.Color.entries.firstOrNull { it.name == contact.iconColor }
|
color = CryptoPortfolioIcon.Color.entries.firstOrNull { it.name == value.iconColor }
|
||||||
?: CryptoPortfolioIcon.Color.Azure,
|
?: CryptoPortfolioIcon.Color.Azure,
|
||||||
),
|
),
|
||||||
networkAddressCount = contact.addresses.size,
|
networkAddressCount = value.addresses.size,
|
||||||
onClick = { onContactClick(contact.id.value) },
|
onClick = { onContactClick(value.id.value) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7,7 +7,6 @@ import com.tangem.domain.addressbook.model.*
|
||||||
import com.tangem.domain.addressbook.usecase.SyncAddressBooksUseCase
|
import com.tangem.domain.addressbook.usecase.SyncAddressBooksUseCase
|
||||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||||
import com.tangem.domain.models.network.Network
|
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.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||||
import com.tangem.features.addressbook.AddressBookContactsBlockComponent
|
import com.tangem.features.addressbook.AddressBookContactsBlockComponent
|
||||||
|
|
@ -48,7 +47,7 @@ internal class ContactsBlockModelTest {
|
||||||
fun resetMocks() {
|
fun resetMocks() {
|
||||||
clearMocks(getVerifiedContactsInteractor, getWalletsUseCase, analyticsSender)
|
clearMocks(getVerifiedContactsInteractor, getWalletsUseCase, analyticsSender)
|
||||||
every { getWalletsUseCase.invokeAsMap(isOnlyMultiCurrency = false, filterLocked = true) } returns
|
every { getWalletsUseCase.invokeAsMap(isOnlyMultiCurrency = false, filterLocked = true) } returns
|
||||||
flowOf(linkedMapOf<UserWalletId, UserWallet>())
|
flowOf(linkedMapOf())
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
|
|
@ -61,7 +60,7 @@ internal class ContactsBlockModelTest {
|
||||||
fun `GIVEN matching contacts WHEN block populated THEN SendFlowWidgetShown sent once`() = runTest {
|
fun `GIVEN matching contacts WHEN block populated THEN SendFlowWidgetShown sent once`() = runTest {
|
||||||
// Arrange
|
// Arrange
|
||||||
every { getVerifiedContactsInteractor.getVerifiedContacts(query = any(), userWalletId = null) } returns
|
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
|
// Act
|
||||||
createModel(testScope = this)
|
createModel(testScope = this)
|
||||||
|
|
@ -91,7 +90,7 @@ internal class ContactsBlockModelTest {
|
||||||
// Arrange
|
// Arrange
|
||||||
var clicked: MatchedContact? = null
|
var clicked: MatchedContact? = null
|
||||||
every { getVerifiedContactsInteractor.getVerifiedContacts(query = any(), userWalletId = null) } returns
|
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 })
|
val model = createModel(testScope = this, onContactClick = { clicked = it })
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
|
@ -103,9 +102,6 @@ internal class ContactsBlockModelTest {
|
||||||
assertThat(clicked?.contactId).isEqualTo("42")
|
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(
|
private fun contact(id: String): Contact = Contact(
|
||||||
id = ContactId(id),
|
id = ContactId(id),
|
||||||
walletId = UserWalletId("a"),
|
walletId = UserWalletId("a"),
|
||||||
|
|
|
||||||
|
|
@ -205,26 +205,23 @@ internal class AddressBookListModelTest {
|
||||||
verify(exactly = 1) { analyticsSender.sendContactSelectedInSend(contactId = "42", scope = any()) }
|
verify(exactly = 1) { analyticsSender.sendContactSelectedInSend(contactId = "42", scope = any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifiedContact(id: String, name: String): VerifiedContact = VerifiedContact(
|
private fun verifiedContact(id: String, name: String): Contact = Contact(
|
||||||
contact = Contact(
|
id = ContactId(id),
|
||||||
id = ContactId(id),
|
walletId = UserWalletId("a"),
|
||||||
walletId = UserWalletId("a"),
|
name = ContactName(name).getOrNull()!!,
|
||||||
name = ContactName(name).getOrNull()!!,
|
icon = "",
|
||||||
icon = "",
|
iconColor = CryptoPortfolioIcon.Color.Azure.name,
|
||||||
iconColor = CryptoPortfolioIcon.Color.Azure.name,
|
createdAt = TIMESTAMP,
|
||||||
createdAt = TIMESTAMP,
|
updatedAt = TIMESTAMP,
|
||||||
updatedAt = TIMESTAMP,
|
addresses = listOf(
|
||||||
addresses = listOf(
|
AddressEntry(
|
||||||
AddressEntry(
|
id = AddressEntryId("e-$id"),
|
||||||
id = AddressEntryId("e-$id"),
|
address = "0xABC",
|
||||||
address = "0xABC",
|
networkId = Network.RawID("ethereum"),
|
||||||
networkId = Network.RawID("ethereum"),
|
memo = null,
|
||||||
memo = null,
|
signature = "sig",
|
||||||
signature = "sig",
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
invalidEntries = emptyList(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun createModel(
|
private fun createModel(
|
||||||
|
|
|
||||||
|
|
@ -133,8 +133,8 @@ internal class UpdateAddressBookListContentTransformerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun transform(
|
private fun transform(
|
||||||
allContacts: List<VerifiedContact>,
|
allContacts: List<Contact>,
|
||||||
matchedContacts: List<VerifiedContact>,
|
matchedContacts: List<Contact>,
|
||||||
selectedWalletId: String? = null,
|
selectedWalletId: String? = null,
|
||||||
query: String = "",
|
query: String = "",
|
||||||
): AddressBookListUM = UpdateAddressBookListContentTransformer(
|
): AddressBookListUM = UpdateAddressBookListContentTransformer(
|
||||||
|
|
@ -156,25 +156,22 @@ internal class UpdateAddressBookListContentTransformerTest {
|
||||||
private fun wallet(id: String, name: String): UserWallet =
|
private fun wallet(id: String, name: String): UserWallet =
|
||||||
MockUserWalletFactory.create().copy(walletId = UserWalletId(stringValue = id), name = name)
|
MockUserWalletFactory.create().copy(walletId = UserWalletId(stringValue = id), name = name)
|
||||||
|
|
||||||
private fun verified(walletId: String, name: String): VerifiedContact = VerifiedContact(
|
private fun verified(walletId: String, name: String): Contact = Contact(
|
||||||
contact = Contact(
|
id = ContactId(name + walletId),
|
||||||
id = ContactId(name + walletId),
|
walletId = UserWalletId(stringValue = walletId),
|
||||||
walletId = UserWalletId(stringValue = walletId),
|
name = requireNotNull(ContactName(name).getOrNull()) { "invalid test name" },
|
||||||
name = requireNotNull(ContactName(name).getOrNull()) { "invalid test name" },
|
icon = "",
|
||||||
icon = "",
|
iconColor = "Azure",
|
||||||
iconColor = "Azure",
|
createdAt = "2026-06-10T14:30:00.000Z",
|
||||||
createdAt = "2026-06-10T14:30:00.000Z",
|
updatedAt = "2026-06-10T14:30:00.000Z",
|
||||||
updatedAt = "2026-06-10T14:30:00.000Z",
|
addresses = listOf(
|
||||||
addresses = listOf(
|
AddressEntry(
|
||||||
AddressEntry(
|
id = AddressEntryId(name),
|
||||||
id = AddressEntryId(name),
|
address = "addr-$name",
|
||||||
address = "addr-$name",
|
networkId = Network.RawID("ethereum"),
|
||||||
networkId = Network.RawID("ethereum"),
|
memo = null,
|
||||||
memo = null,
|
signature = "sig",
|
||||||
signature = "sig",
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
invalidEntries = emptyList(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -102,7 +102,6 @@ internal class SendDestinationModel @Inject constructor(
|
||||||
|
|
||||||
private val contacts: StateFlow<List<Contact>> =
|
private val contacts: StateFlow<List<Contact>> =
|
||||||
getVerifiedContactsInteractor.getVerifiedContacts(query = "", userWalletId = null)
|
getVerifiedContactsInteractor.getVerifiedContacts(query = "", userWalletId = null)
|
||||||
.map { verified -> verified.map { it.contact } }
|
|
||||||
.flowOn(dispatchers.default)
|
.flowOn(dispatchers.default)
|
||||||
.stateIn(modelScope, SharingStarted.Eagerly, emptyList())
|
.stateIn(modelScope, SharingStarted.Eagerly, emptyList())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -390,7 +390,7 @@ internal class SendDestinationModelTest {
|
||||||
validateWalletAddressUseCase(any(), any(), any(), any<List<CryptoCurrencyAddress>>(), any())
|
validateWalletAddressUseCase(any(), any(), any(), any<List<CryptoCurrencyAddress>>(), any())
|
||||||
} returns AddressValidation.Success.Valid.right()
|
} returns AddressValidation.Success.Valid.right()
|
||||||
every { getVerifiedContactsInteractor.getVerifiedContacts(any(), any()) } returns
|
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()
|
val sut = buildModel()
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
|
@ -476,7 +476,7 @@ internal class SendDestinationModelTest {
|
||||||
validateWalletAddressUseCase(any(), any(), any(), any<List<CryptoCurrencyAddress>>(), any())
|
validateWalletAddressUseCase(any(), any(), any(), any<List<CryptoCurrencyAddress>>(), any())
|
||||||
} returns AddressValidation.Success.Valid.right()
|
} returns AddressValidation.Success.Valid.right()
|
||||||
every { getVerifiedContactsInteractor.getVerifiedContacts(any(), any()) } returns
|
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)
|
val sut = buildBlockModel(isAddContactAvailable = model.isAddContactAvailable)
|
||||||
advanceUntilIdle()
|
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(
|
private fun buildContact(name: String = "Alice", address: String = "0xAddr"): Contact = Contact(
|
||||||
id = ContactId("c1"),
|
id = ContactId("c1"),
|
||||||
walletId = testUserWalletId,
|
walletId = testUserWalletId,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue