Updated on 2026-08-14
This commit is contained in:
parent
391e0562cc
commit
12cebc413d
17 changed files with 686 additions and 7 deletions
|
|
@ -16,6 +16,8 @@ interface AddressBookRepository {
|
|||
/** Contacts across all wallets (flattened). Each [Contact] keeps its own [Contact.walletId]. */
|
||||
fun getAllContacts(): Flow<List<Contact>>
|
||||
|
||||
suspend fun getContactsSync(userWalletId: UserWalletId): List<Contact>
|
||||
|
||||
suspend fun getContact(userWalletId: UserWalletId, name: String): Contact?
|
||||
|
||||
suspend fun saveContact(contact: Contact): Either<AddressBookSyncError, Unit>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.domain.addressbook.usecase
|
||||
|
||||
import com.tangem.domain.addressbook.model.ContactId
|
||||
import com.tangem.domain.addressbook.repository.AddressBookRepository
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
/**
|
||||
* Enforces the `network + address` uniqueness rule within a wallet's address book: checks whether the
|
||||
* ([networkId], [address]) pair is already saved and, if so, returns the name of the contact that holds it
|
||||
* so the UI can tell the user under which name it is stored. Returns `null` when the pair is free.
|
||||
*
|
||||
* The same address in a different network is allowed. [excludeContactId] lets an in-place edit skip the
|
||||
* contact currently being edited. Address comparison is exact (matching the in-editor dedup in
|
||||
* `AddValidatedAddressTransformer`), so case-sensitive chains are not falsely flagged.
|
||||
*
|
||||
* Reads the local snapshot ([AddressBookRepository.getContactsSync]) rather than the syncing flow, so validating on
|
||||
* every keystroke/selection change never triggers a backend sync.
|
||||
*/
|
||||
class CheckAddressDuplicateUseCase(
|
||||
private val repository: AddressBookRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: String,
|
||||
address: String,
|
||||
excludeContactId: ContactId? = null,
|
||||
): String? {
|
||||
val contacts = repository.getContactsSync(userWalletId)
|
||||
return contacts
|
||||
.firstOrNull { contact ->
|
||||
contact.id != excludeContactId && contact.addressEntries.any { entry ->
|
||||
entry.networkId.value == networkId && entry.address == address
|
||||
}
|
||||
}
|
||||
?.name?.value
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.domain.addressbook.usecase
|
||||
|
||||
import com.tangem.domain.addressbook.model.Contact
|
||||
import com.tangem.domain.addressbook.model.ContactId
|
||||
import com.tangem.domain.addressbook.repository.AddressBookRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class GetContactByIdUseCase(
|
||||
private val repository: AddressBookRepository,
|
||||
) {
|
||||
|
||||
operator fun invoke(id: ContactId): Flow<Contact?> =
|
||||
repository.getAllContacts().map { contacts -> contacts.find { it.id == id } }
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package com.tangem.domain.addressbook.usecase
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.addressbook.model.*
|
||||
import com.tangem.domain.addressbook.repository.AddressBookRepository
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class CheckAddressDuplicateUseCaseTest {
|
||||
|
||||
private val repository: AddressBookRepository = mockk()
|
||||
private val useCase = CheckAddressDuplicateUseCase(repository)
|
||||
|
||||
private val walletId = UserWalletId("0001")
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(repository)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN network and address already saved WHEN invoke THEN returns owning contact name`() = runTest {
|
||||
// Arrange
|
||||
coEvery { repository.getContactsSync(walletId) } returns listOf(contact("Binance", "0xAAA", ETHEREUM))
|
||||
|
||||
// Act
|
||||
val result = useCase(walletId, networkId = ETHEREUM, address = "0xAAA")
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo("Binance")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN same address in a different network WHEN invoke THEN returns null`() = runTest {
|
||||
// Arrange
|
||||
coEvery { repository.getContactsSync(walletId) } returns listOf(contact("Binance", "0xAAA", ETHEREUM))
|
||||
|
||||
// Act
|
||||
val result = useCase(walletId, networkId = TRON, address = "0xAAA")
|
||||
|
||||
// Assert
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN the pair belongs to the excluded contact WHEN invoke THEN returns null`() = runTest {
|
||||
// Arrange
|
||||
val contact = contact("Binance", "0xAAA", ETHEREUM, id = "id-1")
|
||||
coEvery { repository.getContactsSync(walletId) } returns listOf(contact)
|
||||
|
||||
// Act
|
||||
val result = useCase(walletId, networkId = ETHEREUM, address = "0xAAA", excludeContactId = ContactId("id-1"))
|
||||
|
||||
// Assert
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN free pair WHEN invoke THEN returns null`() = runTest {
|
||||
// Arrange
|
||||
coEvery { repository.getContactsSync(walletId) } returns listOf(contact("Binance", "0xAAA", ETHEREUM))
|
||||
|
||||
// Act
|
||||
val result = useCase(walletId, networkId = ETHEREUM, address = "0xBBB")
|
||||
|
||||
// Assert
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
private fun contact(name: String, address: String, networkId: String, id: String = "id-$name"): Contact = Contact(
|
||||
id = ContactId(id),
|
||||
walletId = walletId,
|
||||
name = requireNotNull(ContactName(name).getOrNull()),
|
||||
icon = "",
|
||||
iconColor = "Azure",
|
||||
createdAt = "2026-01-01T00:00:00.000Z",
|
||||
updatedAt = "2026-01-01T00:00:00.000Z",
|
||||
addressEntries = listOf(
|
||||
AddressEntry(
|
||||
id = AddressEntryId("addr-$name"),
|
||||
address = address,
|
||||
networkId = Network.RawID(networkId),
|
||||
memo = null,
|
||||
signature = "sig",
|
||||
networkName = "Ethereum",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val ETHEREUM = "ethereum"
|
||||
const val TRON = "tron"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package com.tangem.domain.addressbook.usecase
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
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.repository.AddressBookRepository
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class GetContactByIdUseCaseTest {
|
||||
|
||||
private val repository: AddressBookRepository = mockk()
|
||||
private val useCase = GetContactByIdUseCase(repository)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(repository)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN matching id WHEN invoke THEN emits that contact`() = runTest {
|
||||
// Arrange
|
||||
val target = contact("id-2", "Bob")
|
||||
every { repository.getAllContacts() } returns flowOf(listOf(contact("id-1", "Alice"), target))
|
||||
|
||||
// Act
|
||||
val result = useCase(ContactId("id-2")).first()
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(target)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no matching id WHEN invoke THEN emits null`() = runTest {
|
||||
// Arrange
|
||||
every { repository.getAllContacts() } returns flowOf(listOf(contact("id-1", "Alice")))
|
||||
|
||||
// Act
|
||||
val result = useCase(ContactId("missing")).first()
|
||||
|
||||
// Assert
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
private fun contact(id: String, name: String): Contact = Contact(
|
||||
id = ContactId(id),
|
||||
walletId = UserWalletId("0001"),
|
||||
name = requireNotNull(ContactName(name).getOrNull()),
|
||||
icon = "",
|
||||
iconColor = "Azure",
|
||||
createdAt = "2026-01-01T00:00:00.000Z",
|
||||
updatedAt = "2026-01-01T00:00:00.000Z",
|
||||
addressEntries = emptyList(),
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue