Updated on 2026-08-14
This commit is contained in:
commit
15a518c576
19 changed files with 93 additions and 171 deletions
|
|
@ -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<List<VerifiedContact>> {
|
||||
fun getVerifiedContacts(query: String, userWalletId: UserWalletId? = null): Flow<List<Contact>> {
|
||||
return getContacts(query, userWalletId).map { 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? {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -20,9 +20,7 @@ class GetContactByIdUseCase(
|
|||
operator fun invoke(id: ContactId): Flow<Contact?> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Contact>): List<VerifiedContact> {
|
||||
suspend fun verifyContacts(contacts: List<Contact>): List<Contact> {
|
||||
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<VerifyMessagesError, AddressEntriesVerification> {
|
||||
private fun verify(userWallet: UserWallet, contact: Contact): Either<VerifyMessagesError, List<AddressEntry>> {
|
||||
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.
|
||||
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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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<List<Contact>>().map { VerifiedContact(contact = it, invalidEntries = emptyList()) }
|
||||
}
|
||||
coEvery { contactSignatureVerifier.verifyContacts(any()) } answers { firstArg<List<Contact>>() }
|
||||
}
|
||||
|
||||
@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.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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue