Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-06 20:27:22 +01:00
commit a2af7d33ea
6 changed files with 2 additions and 136 deletions

View file

@ -11,13 +11,10 @@ import com.tangem.domain.addressbook.usecase.DeleteContactUseCase
import com.tangem.domain.addressbook.usecase.GetContactByIdUseCase
import com.tangem.domain.addressbook.usecase.GetContactsUseCase
import com.tangem.domain.addressbook.usecase.SyncAddressBooksUseCase
import com.tangem.domain.addressbook.usecase.ValidateContactAddressUseCase
import com.tangem.domain.addressbook.validation.ContactNameValidator
import com.tangem.domain.addressbook.verification.ContactSignatureVerifier
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.tokens.GetNetworkAddressesUseCase
import com.tangem.domain.transaction.usecase.SignUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
import com.tangem.domain.transaction.usecase.VerifySecp256k1MessagesUseCase
import dagger.Module
import dagger.Provides
@ -29,18 +26,6 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
object AddressBookDomainModule {
@Provides
@Singleton
fun provideValidateContactAddressUseCase(
validateWalletAddressUseCase: ValidateWalletAddressUseCase,
getNetworkAddressesUseCase: GetNetworkAddressesUseCase,
): ValidateContactAddressUseCase {
return ValidateContactAddressUseCase(
validateWalletAddressUseCase = validateWalletAddressUseCase,
getNetworkAddressesUseCase = getNetworkAddressesUseCase,
)
}
@Provides
@Singleton
fun provideContactSignatureVerifier(

View file

@ -1,14 +1,11 @@
package com.tangem.domain.addressbook.error
import com.tangem.domain.transaction.error.AddressValidation
import com.tangem.domain.transaction.error.SignHashesError
sealed interface SaveContactError {
data class Name(val error: ContactNameValidationError) : SaveContactError
data class Address(val error: AddressValidation.Error) : SaveContactError
data class Signing(val error: SignHashesError) : SaveContactError
data class Backend(val error: AddressBookSyncError) : SaveContactError

View file

@ -1,37 +0,0 @@
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

@ -1,75 +0,0 @@
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

@ -64,8 +64,7 @@ internal class AddressBookAnalyticsSender @Inject constructor(
/**
* Fired when a save failure is surfaced to the user. [contactId] is set only in edit mode. Validation failures
* ([SaveContactError.Name]/[SaveContactError.Address]) are shown inline rather than as a save error, so they do
* not produce this event.
* ([SaveContactError.Name]) are shown inline rather than as a save error, so they do not produce this event.
*/
fun sendSaveErrorShown(walletId: UserWalletId, contactId: String?, error: SaveContactError) {
val errorType = error.toErrorType() ?: return
@ -85,9 +84,7 @@ internal class AddressBookAnalyticsSender @Inject constructor(
AddressBookSyncError.Network -> ErrorType.Network
else -> ErrorType.Server
}
is SaveContactError.Name,
is SaveContactError.Address,
-> null
is SaveContactError.Name -> null
}
fun sendSaveToButtonClicked() {

View file

@ -172,7 +172,6 @@ internal class AddressBookAnalyticsSenderTest {
),
// Validation failures are shown inline, not as a save error.
SaveErrorModel(error = SaveContactError.Name(mockk()), expectedType = null),
SaveErrorModel(error = SaveContactError.Address(mockk()), expectedType = null),
)
private companion object {