Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-04 12:47:27 +01:00
parent 4390b59231
commit 5f9b1a9d58
22 changed files with 786 additions and 0 deletions

View file

@ -126,5 +126,9 @@
{
"name": "AND_15368_VISA_PAY_REDESIGN",
"version": "undefined"
},
{
"name": "TWI_83_ADDRESS_BOOK_ENABLED",
"version": "undefined"
}
]

View file

@ -0,0 +1,27 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
namespace = "com.tangem.domain.addressbook"
}
dependencies {
api(projects.domain.core)
api(projects.domain.models)
implementation(projects.domain.transaction)
implementation(projects.domain.tokens)
implementation(deps.arrow.core)
implementation(deps.kotlin.coroutines)
implementation(deps.kotlin.serialization)
// region Test libraries
testImplementation(projects.test.core)
testImplementation(projects.test.mock)
// endregion
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain.addressbook.error
import com.tangem.domain.addressbook.model.ContactName
import kotlinx.serialization.Serializable
@Serializable
sealed interface ContactNameValidationError {
@Serializable
data class Format(val error: ContactName.Error) : ContactNameValidationError
/** Another contact in the same wallet already uses this name (case-insensitive). */
@Serializable
data object Duplicate : ContactNameValidationError
}

View file

@ -0,0 +1,10 @@
package com.tangem.domain.addressbook.error
import com.tangem.domain.transaction.error.AddressValidation
sealed interface SaveContactError {
data class Name(val error: ContactNameValidationError) : SaveContactError
data class Address(val error: AddressValidation.Error) : SaveContactError
}

View file

@ -0,0 +1,14 @@
package com.tangem.domain.addressbook.model
import com.tangem.domain.models.network.Network
import kotlinx.serialization.Serializable
/** A single saved address belonging to a [Contact]. */
@Serializable
data class AddressEntry(
val id: AddressEntryId,
val address: String,
val networkId: Network.RawID,
val memo: String?,
val signature: String,
)

View file

@ -0,0 +1,8 @@
package com.tangem.domain.addressbook.model
import kotlinx.serialization.Serializable
/** Client-generated UUID v4 identifier of an [AddressEntry]. */
@Serializable
@JvmInline
value class AddressEntryId(val value: String)

View file

@ -0,0 +1,13 @@
package com.tangem.domain.addressbook.model
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.serialization.Serializable
/** A named address stored in the user's address book for fast access when sending. */
@Serializable
data class Contact(
val id: ContactId,
val walletId: UserWalletId,
val name: ContactName,
val addressEntries: List<AddressEntry>,
)

View file

@ -0,0 +1,8 @@
package com.tangem.domain.addressbook.model
import kotlinx.serialization.Serializable
/** Client-generated UUID v4 identifier of a [Contact]. */
@Serializable
@JvmInline
value class ContactId(val value: String)

View file

@ -0,0 +1,50 @@
package com.tangem.domain.addressbook.model
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import kotlinx.serialization.Serializable
/**
* Validated name of a [Contact].
*
* The only way to obtain an instance is the validating [invoke] factory, which enforces the
* address-book naming rules. Uniqueness within a wallet is **not** enforced here it requires
* access to the repository and lives in `ValidateContactNameUseCase`.
*/
@Serializable
@ConsistentCopyVisibility
data class ContactName private constructor(val value: String) {
@Serializable
sealed interface Error {
@Serializable
data object Empty : Error
@Serializable
data object ExceedsMaxLength : Error
@Serializable
data object InvalidCharacters : Error
}
companion object {
const val MIN_LENGTH = 1
const val MAX_LENGTH = 50
/** Letters, numbers and spaces only — forbids emoji, new lines, tabs, special symbols and html/scripts. */
private val allowedPattern = Regex("^[\\p{L}\\p{N} ]+$")
operator fun invoke(value: String): Either<Error, ContactName> = either {
val trimmed = value.trim()
ensure(trimmed.length >= MIN_LENGTH) { Error.Empty }
ensure(trimmed.length <= MAX_LENGTH) { Error.ExceedsMaxLength }
ensure(allowedPattern.matches(trimmed)) { Error.InvalidCharacters }
ContactName(trimmed)
}
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.domain.addressbook.repository
import com.tangem.domain.addressbook.model.Contact
import com.tangem.domain.addressbook.model.ContactId
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
/** Persistence port for the address book. The implementation is provided by the data layer. */
interface AddressBookRepository {
fun getContacts(userWalletId: UserWalletId): Flow<List<Contact>>
/** Contacts across several wallets, flattened. Each [Contact] keeps its own [Contact.walletId]. */
fun getContacts(userWalletIds: Set<UserWalletId>): Flow<List<Contact>>
suspend fun getContact(userWalletId: UserWalletId, name: String): Contact?
/** Inserts or updates a [contact]. */
suspend fun saveContact(contact: Contact)
suspend fun deleteContact(id: ContactId)
}

View file

@ -0,0 +1,43 @@
package com.tangem.domain.addressbook.usecase
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.domain.addressbook.error.SaveContactError
import com.tangem.domain.addressbook.model.AddressEntry
import com.tangem.domain.addressbook.model.Contact
import com.tangem.domain.addressbook.model.ContactId
import com.tangem.domain.addressbook.repository.AddressBookRepository
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import java.util.UUID
/**
* Creates a new [Contact] with client-generated UUID v4 ids. The name must be valid and unique
* (case-insensitive) within the wallet.
*/
class CreateContactUseCase(
private val repository: AddressBookRepository,
private val validateContactName: ValidateContactNameUseCase,
) {
@Suppress("LongParameterList")
suspend operator fun invoke(
userWalletId: UserWalletId,
name: String,
network: Network,
addressEntries: List<AddressEntry>,
): Either<SaveContactError, Contact> = either {
val validName = validateContactName(userWalletId, name)
.mapLeft(SaveContactError::Name)
.bind()
val contact = Contact(
id = ContactId(UUID.randomUUID().toString()),
walletId = userWalletId,
name = validName,
addressEntries = addressEntries,
)
repository.saveContact(contact)
contact
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.domain.addressbook.usecase
import com.tangem.domain.addressbook.model.ContactId
import com.tangem.domain.addressbook.repository.AddressBookRepository
class DeleteContactUseCase(
private val repository: AddressBookRepository,
) {
suspend operator fun invoke(id: ContactId) = repository.deleteContact(id)
}

View file

@ -0,0 +1,13 @@
package com.tangem.domain.addressbook.usecase
import com.tangem.domain.addressbook.model.Contact
import com.tangem.domain.addressbook.repository.AddressBookRepository
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
class GetContactsUseCase(
private val repository: AddressBookRepository,
) {
operator fun invoke(userWalletIds: Set<UserWalletId>): Flow<List<Contact>> = repository.getContacts(userWalletIds)
}

View file

@ -0,0 +1,37 @@
package com.tangem.domain.addressbook.usecase
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.domain.addressbook.error.ContactNameValidationError
import com.tangem.domain.addressbook.error.SaveContactError
import com.tangem.domain.addressbook.model.AddressEntry
import com.tangem.domain.addressbook.model.Contact
import com.tangem.domain.addressbook.model.ContactName
import com.tangem.domain.addressbook.repository.AddressBookRepository
/**
* Updates an existing [Contact], preserving its contact id. The name is only format-checked
* uniqueness is not re-validated on update. Address entries must be prepared and validated before
* calling this use case.
*/
class UpdateContactUseCase(
private val repository: AddressBookRepository,
) {
suspend operator fun invoke(
contact: Contact,
name: String,
addressEntries: List<AddressEntry>,
): Either<SaveContactError, Contact> = either {
val validName = ContactName(name)
.mapLeft { SaveContactError.Name(ContactNameValidationError.Format(it)) }
.bind()
val updated = contact.copy(
name = validName,
addressEntries = addressEntries,
)
repository.saveContact(updated)
updated
}
}

View file

@ -0,0 +1,37 @@
package com.tangem.domain.addressbook.usecase
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.GetNetworkAddressesUseCase
import com.tangem.domain.transaction.error.AddressValidation
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
/**
* Validates a contact's address for a network, reusing the transaction-layer validation. Self-send
* is allowed since saving one's own address in the book is valid.
*/
class ValidateContactAddressUseCase(
private val validateWalletAddressUseCase: ValidateWalletAddressUseCase,
private val getNetworkAddressesUseCase: GetNetworkAddressesUseCase,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
network: Network,
address: String,
): Either<AddressValidation.Error, Unit> = either {
val senderAddresses = getNetworkAddressesUseCase.invokeSync(
userWalletId = userWalletId,
networkRawId = network.id.rawId,
)
validateWalletAddressUseCase(
userWalletId = userWalletId,
network = network,
address = address,
senderAddresses = senderAddresses,
allowSelfSend = true,
).bind()
}
}

View file

@ -0,0 +1,36 @@
package com.tangem.domain.addressbook.usecase
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import com.tangem.domain.addressbook.error.ContactNameValidationError
import com.tangem.domain.addressbook.model.ContactName
import com.tangem.domain.addressbook.repository.AddressBookRepository
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.first
/**
* Validates a contact name: format rules via [ContactName] plus case-insensitive uniqueness within
* the wallet.
*/
class ValidateContactNameUseCase(
private val repository: AddressBookRepository,
) {
suspend operator fun invoke(
walletId: UserWalletId,
name: String,
): Either<ContactNameValidationError, ContactName> = either {
val validName = ContactName(name)
.mapLeft(ContactNameValidationError::Format)
.bind()
val contacts = repository.getContacts(walletId).first()
val isDuplicate = contacts.any { contact ->
contact.name.value.equals(validName.value, ignoreCase = true)
}
ensure(!isDuplicate) { ContactNameValidationError.Duplicate }
validName
}
}

View file

@ -0,0 +1,66 @@
package com.tangem.domain.addressbook.model
import com.google.common.truth.Truth.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ContactNameTest {
@Test
fun `valid name is accepted and trimmed`() {
val result = ContactName(" Alice 1 ")
assertThat(result.getOrNull()?.value).isEqualTo("Alice 1")
}
@Test
fun `single character name is accepted`() {
assertThat(ContactName("A").isRight()).isTrue()
}
@Test
fun `name of max length is accepted`() {
val name = "a".repeat(ContactName.MAX_LENGTH)
assertThat(ContactName(name).isRight()).isTrue()
}
@Test
fun `blank name is rejected as Empty`() {
assertThat(ContactName(" ").leftOrNull()).isEqualTo(ContactName.Error.Empty)
}
@Test
fun `name exceeding max length is rejected`() {
val name = "a".repeat(ContactName.MAX_LENGTH + 1)
assertThat(ContactName(name).leftOrNull()).isEqualTo(ContactName.Error.ExceedsMaxLength)
}
@Test
fun `emoji is rejected`() {
assertThat(ContactName("Alice 😀").leftOrNull()).isEqualTo(ContactName.Error.InvalidCharacters)
}
@Test
fun `new line is rejected`() {
assertThat(ContactName("Ali\nce").leftOrNull()).isEqualTo(ContactName.Error.InvalidCharacters)
}
@Test
fun `tab is rejected`() {
assertThat(ContactName("Ali\tce").leftOrNull()).isEqualTo(ContactName.Error.InvalidCharacters)
}
@Test
fun `html script is rejected`() {
assertThat(ContactName("<script>alert(1)</script>").leftOrNull())
.isEqualTo(ContactName.Error.InvalidCharacters)
}
@Test
fun `special symbols are rejected`() {
assertThat(ContactName("Alice@!").leftOrNull()).isEqualTo(ContactName.Error.InvalidCharacters)
}
}

View file

@ -0,0 +1,122 @@
package com.tangem.domain.addressbook.usecase
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.addressbook.error.ContactNameValidationError
import com.tangem.domain.addressbook.error.SaveContactError
import com.tangem.domain.addressbook.model.AddressEntry
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.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.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
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 CreateContactUseCaseTest {
private val repository: AddressBookRepository = mockk(relaxUnitFun = true)
private val useCase = CreateContactUseCase(
repository = repository,
validateContactName = ValidateContactNameUseCase(repository),
)
private val walletId = UserWalletId("011")
private val networkRawId = Network.RawID("ethereum")
private val networkId = Network.ID(value = "ethereum", derivationPath = Network.DerivationPath.None)
private val network: Network = mockk { every { id } returns networkId }
private val addressEntries = listOf(
AddressEntry(
id = AddressEntryId("addr-1"),
address = "0xabc",
networkId = networkRawId,
memo = "memo",
signature = "sig",
),
)
@BeforeEach
fun resetMocks() {
clearMocks(repository)
}
@Test
fun `create generates ids and persists the contact`() = runTest {
every { repository.getContacts(walletId) } returns flowOf(emptyList())
val saved = slot<Contact>()
coEvery { repository.saveContact(capture(saved)) } returns Unit
val result = useCase(
userWalletId = walletId,
name = "Alice",
network = network,
addressEntries = addressEntries,
)
val contact = result.getOrNull()
assertThat(contact).isEqualTo(saved.captured)
assertThat(contact!!.walletId).isEqualTo(walletId)
assertThat(contact.name.value).isEqualTo("Alice")
assertThat(contact.id.value).isNotEmpty()
assertThat(contact.addressEntries).isEqualTo(addressEntries)
}
@Test
fun `duplicate name fails without persisting`() = runTest {
every { repository.getContacts(walletId) } returns flowOf(listOf(contact(name = "Alice")))
val result = useCase(
userWalletId = walletId,
name = "alice",
network = network,
addressEntries = addressEntries,
)
assertThat(result.leftOrNull())
.isEqualTo(SaveContactError.Name(ContactNameValidationError.Duplicate))
coVerify(exactly = 0) { repository.saveContact(any()) }
}
@Test
fun `invalid name fails without persisting`() = runTest {
every { repository.getContacts(walletId) } returns flowOf(emptyList())
val result = useCase(
userWalletId = walletId,
name = "",
network = network,
addressEntries = addressEntries,
)
assertThat(result.leftOrNull())
.isEqualTo(SaveContactError.Name(ContactNameValidationError.Format(ContactName.Error.Empty)))
coVerify(exactly = 0) { repository.saveContact(any()) }
}
private fun contact(name: String): Contact = Contact(
id = ContactId("id-$name"),
walletId = walletId,
name = requireNotNull(ContactName(name).getOrNull()),
addressEntries = listOf(
AddressEntry(
id = AddressEntryId("addr-$name"),
address = "0xabc",
networkId = networkRawId,
memo = null,
signature = "sig",
),
),
)
}

View file

@ -0,0 +1,97 @@
package com.tangem.domain.addressbook.usecase
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.addressbook.error.ContactNameValidationError
import com.tangem.domain.addressbook.error.SaveContactError
import com.tangem.domain.addressbook.model.AddressEntry
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.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.coVerify
import io.mockk.mockk
import io.mockk.slot
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 UpdateContactUseCaseTest {
private val repository: AddressBookRepository = mockk(relaxUnitFun = true)
private val useCase = UpdateContactUseCase(
repository = repository,
)
private val walletId = UserWalletId("011")
private val networkRawId = Network.RawID("ethereum")
private val updatedEntries = listOf(
AddressEntry(
id = AddressEntryId("addr-new"),
address = "0xnew",
networkId = networkRawId,
memo = "memo",
signature = "sig2",
),
)
@BeforeEach
fun resetMocks() {
clearMocks(repository)
}
@Test
fun `update preserves id and persists changes without checking uniqueness`() = runTest {
val existing = contact(name = "Alice")
val saved = slot<Contact>()
coEvery { repository.saveContact(capture(saved)) } returns Unit
val result = useCase(
contact = existing,
name = "Bob",
addressEntries = updatedEntries,
)
val contact = result.getOrNull()
assertThat(contact).isEqualTo(saved.captured)
assertThat(contact!!.id).isEqualTo(existing.id)
assertThat(contact.name.value).isEqualTo("Bob")
assertThat(contact.addressEntries).isEqualTo(updatedEntries)
coVerify(exactly = 0) { repository.getContacts(any<UserWalletId>()) }
}
@Test
fun `invalid name fails without persisting`() = runTest {
val result = useCase(
contact = contact(name = "Alice"),
name = "",
addressEntries = updatedEntries,
)
assertThat(result.leftOrNull())
.isEqualTo(SaveContactError.Name(ContactNameValidationError.Format(ContactName.Error.Empty)))
coVerify(exactly = 0) { repository.saveContact(any()) }
}
private fun contact(name: String): Contact = Contact(
id = ContactId("id-$name"),
walletId = walletId,
name = requireNotNull(ContactName(name).getOrNull()),
addressEntries = listOf(
AddressEntry(
id = AddressEntryId("addr-$name"),
address = "0xabc",
networkId = networkRawId,
memo = null,
signature = "sig",
),
),
)
}

View file

@ -0,0 +1,75 @@
package com.tangem.domain.addressbook.usecase
import arrow.core.left
import arrow.core.right
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.models.network.CryptoCurrencyAddress
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.GetNetworkAddressesUseCase
import com.tangem.domain.transaction.error.AddressValidation
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
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 ValidateContactAddressUseCaseTest {
private val validateWalletAddressUseCase: ValidateWalletAddressUseCase = mockk()
private val getNetworkAddressesUseCase: GetNetworkAddressesUseCase = mockk()
private val useCase = ValidateContactAddressUseCase(
validateWalletAddressUseCase = validateWalletAddressUseCase,
getNetworkAddressesUseCase = getNetworkAddressesUseCase,
)
private val walletId = UserWalletId("011")
private val networkRawId = Network.RawID("ethereum")
private val networkId = Network.ID(value = "ethereum", derivationPath = Network.DerivationPath.None)
private val network: Network = mockk { every { id } returns networkId }
@BeforeEach
fun resetMocks() {
clearMocks(validateWalletAddressUseCase, getNetworkAddressesUseCase)
}
@Test
fun `valid address forwards sender addresses and allows self-send`() = runTest {
val senderAddresses = listOf<CryptoCurrencyAddress>(mockk())
coEvery { getNetworkAddressesUseCase.invokeSync(walletId, networkRawId) } returns senderAddresses
coEvery {
validateWalletAddressUseCase(any(), any(), any(), any<List<CryptoCurrencyAddress>>(), any())
} returns AddressValidation.Success.Valid.right()
val result = useCase(walletId, network, "0xabc")
assertThat(result.isRight()).isTrue()
coVerify {
validateWalletAddressUseCase(
userWalletId = walletId,
network = network,
address = "0xabc",
senderAddresses = senderAddresses,
allowSelfSend = true,
)
}
}
@Test
fun `invalid address propagates the validation error`() = runTest {
coEvery { getNetworkAddressesUseCase.invokeSync(walletId, networkRawId) } returns emptyList()
coEvery {
validateWalletAddressUseCase(any(), any(), any(), any<List<CryptoCurrencyAddress>>(), any())
} returns AddressValidation.Error.InvalidAddress.left()
val result = useCase(walletId, network, "bad")
assertThat(result.leftOrNull()).isEqualTo(AddressValidation.Error.InvalidAddress)
}
}

View file

@ -0,0 +1,77 @@
package com.tangem.domain.addressbook.usecase
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.addressbook.error.ContactNameValidationError
import com.tangem.domain.addressbook.model.AddressEntry
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.repository.AddressBookRepository
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import io.mockk.clearMocks
import io.mockk.every
import io.mockk.mockk
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 ValidateContactNameUseCaseTest {
private val repository: AddressBookRepository = mockk(relaxUnitFun = true)
private val useCase = ValidateContactNameUseCase(repository)
private val walletId = UserWalletId("011")
@BeforeEach
fun resetMocks() {
clearMocks(repository)
}
@Test
fun `format error is propagated`() = runTest {
every { repository.getContacts(walletId) } returns flowOf(emptyList())
val result = useCase(walletId, name = "")
assertThat(result.leftOrNull())
.isEqualTo(ContactNameValidationError.Format(ContactName.Error.Empty))
}
@Test
fun `duplicate name in same wallet is rejected case-insensitively`() = runTest {
every { repository.getContacts(walletId) } returns flowOf(listOf(contact(name = "Alice")))
val result = useCase(walletId, name = "alice")
assertThat(result.leftOrNull()).isEqualTo(ContactNameValidationError.Duplicate)
}
@Test
fun `unique name is accepted`() = runTest {
every { repository.getContacts(walletId) } returns flowOf(listOf(contact(name = "Alice")))
val result = useCase(walletId, name = "Bob")
assertThat(result.getOrNull()?.value).isEqualTo("Bob")
}
private fun contact(name: String): Contact = Contact(
id = ContactId("id-$name"),
walletId = walletId,
name = requireNotNull(ContactName(name).getOrNull()),
addressEntries = listOf(
AddressEntry(
id = AddressEntryId("addr-$name"),
address = "0xabc",
networkId = Network.RawID("ethereum"),
memo = null,
signature = "sig",
),
),
)
}

View file

@ -342,6 +342,7 @@ include(":domain:legacy")
include(":domain:account")
include(":domain:account:status")
include(":domain:address-book")
include(":domain:card")
include(":domain:common")
include(":domain:core")