Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-03 12:29:53 +01:00
parent 3e47825909
commit 1f00384c0c
22 changed files with 59 additions and 63 deletions

View file

@ -8,7 +8,6 @@ import com.tangem.data.common.cache.etag.ETagsStore
import com.tangem.datasource.api.addressbook.AddressBookApi
import com.tangem.datasource.api.addressbook.models.SyncAddressBooksRequest
import com.tangem.datasource.api.addressbook.models.SyncAddressBooksResponse
import com.tangem.datasource.api.addressbook.models.UpdateAddressBookRequest
import com.tangem.datasource.api.addressbook.models.UpdateAddressBookResponse
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
@ -391,7 +390,7 @@ internal class DefaultAddressBookRepositoryTest {
iconColor = iconColor,
createdAt = TIMESTAMP,
updatedAt = TIMESTAMP,
addressEntries = emptyList(),
addresses = emptyList(),
)
private fun createBlob(): AddressBookBlob = AddressBookBlob(

View file

@ -29,7 +29,7 @@ class GetVerifiedContactsInteractor(
val userWallet = walletsById[contact.walletId] ?: return@mapNotNull null
val verification = verify(userWallet, contact).getOrNull() ?: return@mapNotNull null
VerifiedContact(
contact = contact.copy(addressEntries = verification.valid),
contact = contact.copy(addresses = verification.valid),
invalidEntries = verification.invalid,
)
}
@ -40,7 +40,7 @@ class GetVerifiedContactsInteractor(
userWallet: UserWallet,
contact: Contact,
): Either<VerifyMessagesError, AddressEntriesVerification> {
val entries = contact.addressEntries
val entries = contact.addresses
if (entries.isEmpty()) return AddressEntriesVerification(valid = emptyList(), invalid = emptyList()).right()
// Entries with a malformed (non-hex) signature can't be verified — they are invalid by format.

View file

@ -31,7 +31,7 @@ class SaveContactInteractor(
userWallet: UserWallet,
name: String,
iconColor: String,
addressEntries: List<AddressEntry>,
addresses: List<AddressEntry>,
): Either<SaveContactError, Contact> = either {
val userWalletId = userWallet.walletId
val validName = validateContactName(userWalletId, name)
@ -47,9 +47,9 @@ class SaveContactInteractor(
iconColor = iconColor,
createdAt = now,
updatedAt = now,
addressEntries = addressEntries,
addresses = addresses,
)
val signed = signAddressEntries(userWallet, contact)
val signed = signAddresses(userWallet, contact)
.mapLeft(SaveContactError::Signing)
.bind()
repository.saveContact(signed)
@ -63,7 +63,7 @@ class SaveContactInteractor(
contact: Contact,
name: String,
iconColor: String,
addressEntries: List<AddressEntry>,
addresses: List<AddressEntry>,
): Either<SaveContactError, Contact> = either {
val validName = ContactName(name)
.mapLeft { SaveContactError.Name(ContactNameValidationError.Format(it)) }
@ -72,10 +72,10 @@ class SaveContactInteractor(
val updated = contact.copy(
name = validName,
iconColor = iconColor,
addressEntries = addressEntries,
addresses = addresses,
updatedAt = timestampProvider.now(),
)
val signed = signAddressEntries(userWallet, updated)
val signed = signAddresses(userWallet, updated)
.mapLeft(SaveContactError::Signing)
.bind()
repository.saveContact(signed)
@ -84,11 +84,9 @@ class SaveContactInteractor(
signed
}
private suspend fun signAddressEntries(
userWallet: UserWallet,
contact: Contact,
): Either<SignHashesError, Contact> = either {
val entries = contact.addressEntries
private suspend fun signAddresses(userWallet: UserWallet, contact: Contact): Either<SignHashesError, Contact> =
either {
val entries = contact.addresses
if (entries.isEmpty()) return@either contact
val publicKey = userWallet.primarySecp256k1PublicKey() ?: raise(SignHashesError.NoSigningKey)
@ -98,7 +96,7 @@ class SaveContactInteractor(
val signedEntries = entries.mapIndexed { index, entry ->
entry.copy(signature = signatures[index].toHexString())
}
contact.copy(addressEntries = signedEntries)
contact.copy(addresses = signedEntries)
}
private fun hashEntry(contact: Contact, entry: AddressEntry): ByteArray {

View file

@ -19,5 +19,5 @@ data class Contact(
val iconColor: String,
val createdAt: String,
val updatedAt: String,
val addressEntries: List<AddressEntry>,
val addresses: List<AddressEntry>,
)

View file

@ -29,7 +29,7 @@ class CheckAddressDuplicateUseCase(
val contacts = repository.getContactsSync(userWalletId)
return contacts
.firstOrNull { contact ->
contact.id != excludeContactId && contact.addressEntries.any { entry ->
contact.id != excludeContactId && contact.addresses.any { entry ->
entry.networkId.value == networkId && entry.address == address
}
}

View file

@ -29,7 +29,7 @@ class GetContactsUseCase(
private fun Contact.matches(query: String): Boolean {
val isNameContaining = name.value.contains(other = query, ignoreCase = true)
val isAddressContaining = addressEntries.any { addressEntry ->
val isAddressContaining = addresses.any { addressEntry ->
addressEntry.address.contains(other = query, ignoreCase = true)
}
return isNameContaining || isAddressContaining

View file

@ -272,7 +272,7 @@ internal class AddressBookCipherTest {
iconColor = iconColor,
createdAt = "2026-01-01T00:00:00.000Z",
updatedAt = "2026-05-22T09:00:00.000Z",
addressEntries = entries.toList(),
addresses = entries.toList(),
)
private fun entry(id: String, address: String, memo: String?): AddressEntry = AddressEntry(

View file

@ -67,7 +67,7 @@ class GetVerifiedContactsInteractorTest {
// Assert
assertThat(result).containsExactly(
VerifiedContact(
contact = contact.copy(addressEntries = listOf(valid)),
contact = contact.copy(addresses = listOf(valid)),
invalidEntries = listOf(invalid),
),
)
@ -93,8 +93,8 @@ class GetVerifiedContactsInteractorTest {
// Assert
assertThat(messagesSlot.captured.map { String(it) })
.containsExactly(
expectedPayload(contact, contact.addressEntries[0]),
expectedPayload(contact, contact.addressEntries[1]),
expectedPayload(contact, contact.addresses[0]),
expectedPayload(contact, contact.addresses[1]),
)
.inOrder()
assertThat(signaturesSlot.captured.map { it.toHexString() }).containsExactly("AABB", "CCDD").inOrder()
@ -114,7 +114,7 @@ class GetVerifiedContactsInteractorTest {
val result = interactor(query = "").first().single()
// Assert
assertThat(result.contact.addressEntries).containsExactly(valid1, valid2).inOrder()
assertThat(result.contact.addresses).containsExactly(valid1, valid2).inOrder()
assertThat(result.invalidEntries).containsExactly(invalid)
}
@ -135,7 +135,7 @@ class GetVerifiedContactsInteractorTest {
// Assert
assertThat(signaturesSlot.captured.map { it.toHexString() }).containsExactly("AABB")
assertThat(result.contact.addressEntries).containsExactly(signed)
assertThat(result.contact.addresses).containsExactly(signed)
assertThat(result.invalidEntries).containsExactly(malformed)
}
@ -149,7 +149,7 @@ class GetVerifiedContactsInteractorTest {
val result = interactor(query = "").first().single()
// Assert
assertThat(result.contact.addressEntries).isEmpty()
assertThat(result.contact.addresses).isEmpty()
assertThat(result.invalidEntries).isEmpty()
verify(exactly = 0) { verifyMessages(any(), any(), any()) }
}
@ -192,7 +192,7 @@ class GetVerifiedContactsInteractorTest {
iconColor = "KekColor",
createdAt = "2026-01-01T00:00:00.000Z",
updatedAt = "2026-01-01T00:00:00.000Z",
addressEntries = entries.toList(),
addresses = entries.toList(),
)
private fun entry(id: String, address: String, memo: String?, signature: String): AddressEntry = AddressEntry(

View file

@ -87,7 +87,7 @@ internal class SaveContactInteractorTest {
assertThat(contact.id.value).isNotEmpty()
assertThat(contact.createdAt).isEqualTo(NEW_TIMESTAMP)
assertThat(contact.updatedAt).isEqualTo(NEW_TIMESTAMP)
assertThat(contact.addressEntries.map { it.signature })
assertThat(contact.addresses.map { it.signature })
.containsExactly(signatures[0].toHexString())
}
@ -121,7 +121,7 @@ internal class SaveContactInteractorTest {
expectedHash(persisted, twoEntries[1]).toHexString(),
)
.inOrder()
assertThat(persisted.addressEntries.map { it.signature })
assertThat(persisted.addresses.map { it.signature })
.containsExactly(signatures[0].toHexString(), signatures[1].toHexString())
.inOrder()
}
@ -138,7 +138,7 @@ internal class SaveContactInteractorTest {
// Assert
assertThat(result.getOrNull()).isEqualTo(saved.captured)
assertThat(saved.captured.addressEntries).isEmpty()
assertThat(saved.captured.addresses).isEmpty()
coVerify(exactly = 0) { signUseCase(any<List<ByteArray>>(), any(), any()) }
}
@ -249,7 +249,7 @@ internal class SaveContactInteractorTest {
contact = existing,
name = "Bob",
iconColor = "TestColor",
addressEntries = updatedEntries,
addresses = updatedEntries,
)
// Assert
@ -259,7 +259,7 @@ internal class SaveContactInteractorTest {
assertThat(contact.name.value).isEqualTo("Bob")
assertThat(contact.createdAt).isEqualTo(ORIGINAL_TIMESTAMP)
assertThat(contact.updatedAt).isEqualTo(NEW_TIMESTAMP)
assertThat(contact.addressEntries.map { it.signature })
assertThat(contact.addresses.map { it.signature })
.containsExactly(signatures[0].toHexString())
coVerify(exactly = 0) { repository.getContacts(any<UserWalletId>()) }
}
@ -276,7 +276,7 @@ internal class SaveContactInteractorTest {
contact = contact(name = "Alice"),
name = "Bob",
iconColor = "TestColor",
addressEntries = updatedEntries,
addresses = updatedEntries,
)
// Assert
@ -292,7 +292,7 @@ internal class SaveContactInteractorTest {
contact = contact(name = "Alice"),
name = "",
iconColor = "TestColor",
addressEntries = updatedEntries,
addresses = updatedEntries,
)
// Assert
@ -310,7 +310,7 @@ internal class SaveContactInteractorTest {
iconColor = "TestColor",
createdAt = ORIGINAL_TIMESTAMP,
updatedAt = ORIGINAL_TIMESTAMP,
addressEntries = listOf(entry(id = "addr-$name", address = "0xabc", memo = null)),
addresses = listOf(entry(id = "addr-$name", address = "0xabc", memo = null)),
)
private fun entry(id: String, address: String, memo: String?): AddressEntry = AddressEntry(

View file

@ -83,7 +83,7 @@ class CheckAddressDuplicateUseCaseTest {
iconColor = "Azure",
createdAt = "2026-01-01T00:00:00.000Z",
updatedAt = "2026-01-01T00:00:00.000Z",
addressEntries = listOf(
addresses = listOf(
AddressEntry(
id = AddressEntryId("addr-$name"),
address = address,

View file

@ -60,6 +60,6 @@ class GetContactByIdUseCaseTest {
iconColor = "Azure",
createdAt = "2026-01-01T00:00:00.000Z",
updatedAt = "2026-01-01T00:00:00.000Z",
addressEntries = emptyList(),
addresses = emptyList(),
)
}

View file

@ -112,7 +112,7 @@ class GetContactsUseCaseTest {
iconColor = "KekColor",
createdAt = createdAt,
updatedAt = "2026-01-01T00:00:00.000Z",
addressEntries = listOf(
addresses = listOf(
AddressEntry(
id = AddressEntryId("addr-$name"),
address = address,

View file

@ -68,7 +68,7 @@ class ValidateContactNameUseCaseTest {
iconColor = "KekColor",
createdAt = "2026-01-01T00:00:00.000Z",
updatedAt = "2026-01-01T00:00:00.000Z",
addressEntries = listOf(
addresses = listOf(
AddressEntry(
id = AddressEntryId("addr-$name"),
address = "0xabc",

View file

@ -14,7 +14,7 @@ internal object ContactMatcher {
fun match(contacts: List<Contact>, networkId: String): List<MatchedContact> {
return contacts.mapNotNull { contact ->
val entries = contact.addressEntries.filter { it.networkId.value == networkId }
val entries = contact.addresses.filter { it.networkId.value == networkId }
if (entries.isEmpty()) return@mapNotNull null
MatchedContact(

View file

@ -180,7 +180,7 @@ internal class EditContactModel @Inject constructor(
stateController.update(UpdateContactNameTransformer(name = contact.name.value))
CryptoPortfolioIcon.Color.entries.firstOrNull { it.name == contact.iconColor }
?.let { stateController.update(SelectContactColorTransformer(color = it)) }
val addresses = ContactAddressEntriesConverter().toValidatedAddresses(contact.addressEntries)
val addresses = ContactAddressEntriesConverter().toValidatedAddresses(contact.addresses)
stateController.update(SetValidatedAddressesTransformer(addresses = addresses, maxAddresses = MAX_ADDRESSES))
}
@ -298,7 +298,7 @@ internal class EditContactModel @Inject constructor(
if (saveJob?.isActive == true) return
val userWallet = selectedWallet.value ?: return
val ui = stateController.uiState.value
val addressEntries = ContactAddressEntriesConverter().convert(ui.addresses)
val addresses = ContactAddressEntriesConverter().convert(ui.addresses)
val existing = loadedContact.value
saveJob = modelScope.launch {
@ -308,14 +308,14 @@ internal class EditContactModel @Inject constructor(
contact = existing,
name = ui.name,
iconColor = ui.colors.selected.name,
addressEntries = addressEntries,
addresses = addresses,
)
} else {
saveContactInteractor.createContact(
userWallet = userWallet,
name = ui.name,
iconColor = ui.colors.selected.name,
addressEntries = addressEntries,
addresses = addresses,
)
}
result.fold(
@ -514,7 +514,7 @@ internal class EditContactModel @Inject constructor(
private fun Contact.toSnapshot(): EditSnapshot = EditSnapshot(
name = name.value,
colorName = iconColor,
addresses = ContactAddressEntriesConverter().toValidatedAddresses(addressEntries)
addresses = ContactAddressEntriesConverter().toValidatedAddresses(addresses)
.map { it.address to it.networkIds.toSet() },
)

View file

@ -23,7 +23,7 @@ internal class DefaultContactConverter(
color = CryptoPortfolioIcon.Color.entries.firstOrNull { it.name == contact.iconColor }
?: CryptoPortfolioIcon.Color.Azure,
),
networkAddressCount = contact.addressEntries.size,
networkAddressCount = contact.addresses.size,
onClick = { onContactClick(contact.id.value) },
)
}

View file

@ -81,7 +81,7 @@ internal class ContactMatcherTest {
iconColor = iconColor,
createdAt = "2026-06-10T14:30:00.000Z",
updatedAt = "2026-06-10T14:30:00.000Z",
addressEntries = entries.toList(),
addresses = entries.toList(),
)
private fun entry(address: String, networkId: String, memo: String? = null): AddressEntry = AddressEntry(

View file

@ -526,7 +526,7 @@ internal class EditContactModelTest {
userWallet = walletA,
name = "Satoshi",
iconColor = any(),
addressEntries = any(),
addresses = any(),
)
}
verify(exactly = 1) {
@ -830,7 +830,7 @@ internal class EditContactModelTest {
val walletA = createWallet(id = "aa", name = "Wallet A")
setupWallets(wallets = listOf(walletA), selected = walletA)
val contact = existingContact(walletId = "aa", name = "Alice", address = "0xAAA").copy(
addressEntries = listOf(
addresses = listOf(
AddressEntry(
id = AddressEntryId("e-1"),
address = "0xAAA",
@ -891,7 +891,7 @@ internal class EditContactModelTest {
iconColor = CryptoPortfolioIcon.Color.Azure.name,
createdAt = "2026-01-01T00:00:00.000Z",
updatedAt = "2026-01-01T00:00:00.000Z",
addressEntries = listOf(
addresses = listOf(
AddressEntry(
id = AddressEntryId("e-1"),
address = address,

View file

@ -2,7 +2,6 @@ package com.tangem.features.addressbook.list.model
import com.google.common.truth.Truth.assertThat
import com.tangem.core.decompose.model.MutableParamsContainer
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.AddressEntry
@ -129,7 +128,7 @@ internal class AddressBookListModelTest {
iconColor = CryptoPortfolioIcon.Color.Azure.name,
createdAt = TIMESTAMP,
updatedAt = TIMESTAMP,
addressEntries = listOf(
addresses = listOf(
AddressEntry(
id = AddressEntryId("e-$id"),
address = "0xABC",

View file

@ -165,7 +165,7 @@ internal class UpdateAddressBookListContentTransformerTest {
iconColor = "Azure",
createdAt = "2026-06-10T14:30:00.000Z",
updatedAt = "2026-06-10T14:30:00.000Z",
addressEntries = listOf(
addresses = listOf(
AddressEntry(
id = AddressEntryId(name),
address = "addr-$name",

View file

@ -124,7 +124,7 @@ internal class SendDestinationModel @Inject constructor(
if (address.contactName != null) return@combine false // sent via a contact
val networkId = cryptoCurrency.network.rawId
contactList.none { contact ->
contact.addressEntries.any {
contact.addresses.any {
it.networkId.value == networkId && it.address.equals(address.actualAddress, ignoreCase = true)
}
}
@ -481,7 +481,7 @@ internal class SendDestinationModel @Inject constructor(
private fun findContactByAddress(address: String): Contact? {
val networkId = cryptoCurrency.network.rawId
return contacts.value.firstOrNull { contact ->
contact.addressEntries.any { entry ->
contact.addresses.any { entry ->
entry.networkId.value == networkId && entry.address.equals(address, ignoreCase = true)
}
}

View file

@ -489,7 +489,7 @@ internal class SendDestinationModelTest {
iconColor = "#FFFFFF",
createdAt = "2026-01-01T00:00:00.000Z",
updatedAt = "2026-01-01T00:00:00.000Z",
addressEntries = listOf(
addresses = listOf(
AddressEntry(
id = AddressEntryId("e1"),
address = address,