From 03b3a7d0a29cbb6326e8cb1d5e668a196f22987e Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 13 Oct 2025 12:54:25 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/di/domain/AccountDomainModule.kt | 6 +- .../api/tangemTech/TangemTechApi.kt | 2 +- .../fetcher/DefaultWalletAccountsFetcher.kt | 28 ++++++---- .../FetchWalletAccountsErrorHandler.kt | 23 +++++--- .../DefaultAccountsCRUDRepository.kt | 12 ++-- .../DefaultWalletAccountsFetcherTest.kt | 52 +++-------------- .../FetchWalletAccountsErrorHandlerTest.kt | 38 ++++--------- .../DefaultAccountsCRUDRepositoryTest.kt | 50 ++++++----------- .../common/account/WalletAccountsSaver.kt | 8 +-- .../usecase/RecoverCryptoPortfolioUseCase.kt | 56 +++++++++---------- .../RecoverCryptoPortfolioUseCaseTest.kt | 34 ++++++----- .../archived/ArchivedAccountListModel.kt | 40 +++++++++++-- 12 files changed, 168 insertions(+), 181 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt index 34f9a1101c..7c946ee340 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt @@ -49,8 +49,12 @@ internal object AccountDomainModule { @Singleton fun provideRecoverCryptoPortfolioUseCase( accountsCRUDRepository: AccountsCRUDRepository, + mainAccountTokensMigration: MainAccountTokensMigration, ): RecoverCryptoPortfolioUseCase { - return RecoverCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository) + return RecoverCryptoPortfolioUseCase( + crudRepository = accountsCRUDRepository, + mainAccountTokensMigration = mainAccountTokensMigration, + ) } @Provides diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 3b08465384..e838002aa8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -156,7 +156,7 @@ interface TangemTechApi { @Path("walletId") walletId: String, @Header("If-Match") eTag: String, @Body body: SaveWalletAccountsResponse, - ): ApiResponse + ): ApiResponse @GET("/v1/wallets/{walletId}/accounts/archived") suspend fun getWalletArchivedAccounts( diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt index 5740e30ccb..200e133dbd 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt @@ -63,23 +63,24 @@ internal class DefaultWalletAccountsFetcher @Inject constructor( } } - override suspend fun pushAndStore(userWalletId: UserWalletId, response: GetWalletAccountsResponse) { - push(userWalletId = userWalletId, accounts = response.accounts) - store(userWalletId = userWalletId, response = response) - } - override suspend fun store(userWalletId: UserWalletId, response: GetWalletAccountsResponse) { val store = getAccountsResponseStore(userWalletId = userWalletId) store.updateData { response } } - override suspend fun push(userWalletId: UserWalletId, accounts: List) { - push(userWalletId = userWalletId, body = SaveWalletAccountsResponse(accounts = accounts)) + override suspend fun push( + userWalletId: UserWalletId, + accounts: List, + ): GetWalletAccountsResponse? { + return push(userWalletId = userWalletId, body = SaveWalletAccountsResponse(accounts = accounts)) } - override suspend fun push(userWalletId: UserWalletId, body: SaveWalletAccountsResponse) { - safeApiCall( + override suspend fun push( + userWalletId: UserWalletId, + body: SaveWalletAccountsResponse, + ): GetWalletAccountsResponse? { + return safeApiCall( call = { var eTag = getETag(userWalletId) @@ -105,6 +106,8 @@ internal class DefaultWalletAccountsFetcher @Inject constructor( if (error.isNetworkError(code = Code.PRECONDITION_FAILED)) { throw error } + + null }, ) } @@ -152,7 +155,12 @@ internal class DefaultWalletAccountsFetcher @Inject constructor( ), ) - pushAndStore(userWalletId, response) + userTokensSaver.push(userWalletId = userWalletId, response = response.toUserTokensResponse()) + val syncedResponse = push(userWalletId = userWalletId, accounts = response.accounts) + + if (syncedResponse != null) { + store(userWalletId = userWalletId, response = syncedResponse) + } } private suspend fun assignTokens(userWalletId: UserWalletId, accountsResponse: GetWalletAccountsResponse) { diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt index 6f284edf5b..0d9ea8c585 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt @@ -47,8 +47,8 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor( error: ApiResponseError, userWalletId: UserWalletId, savedAccountsResponse: GetWalletAccountsResponse?, - pushWalletAccounts: suspend (userWalletId: UserWalletId, accounts: List) -> Unit, - storeWalletAccounts: suspend (userWalletId: UserWalletId, response: GetWalletAccountsResponse) -> Unit, + pushWalletAccounts: suspend (UserWalletId, List) -> GetWalletAccountsResponse?, + storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit, ): GetWalletAccountsResponse? { val isResponseUpToDate = error.isNetworkError(code = Code.NOT_MODIFIED) if (isResponseUpToDate) { @@ -56,17 +56,17 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor( return savedAccountsResponse } - val response = savedAccountsResponse ?: defaultWalletAccountsResponseFactory.create( - userWalletId = userWalletId, - userTokensResponse = getFromLegacyStore(userWalletId), - ) - + var response = savedAccountsResponse ?: createDefaultResponse(userWalletId) val (accountDTOs, userTokensResponse) = response.accounts to response.toUserTokensResponse() val isNotFoundError = error.isNetworkError(code = Code.NOT_FOUND) if (isNotFoundError) { - pushWalletAccounts(userWalletId, accountDTOs) userTokensSaver.push(userWalletId = userWalletId, response = userTokensResponse) + val updatedResponse = pushWalletAccounts(userWalletId, accountDTOs) + + if (updatedResponse != null) { + response = updatedResponse + } } storeWalletAccounts(userWalletId, response) @@ -74,6 +74,13 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor( return response } + private suspend fun createDefaultResponse(userWalletId: UserWalletId): GetWalletAccountsResponse { + return defaultWalletAccountsResponseFactory.create( + userWalletId = userWalletId, + userTokensResponse = getFromLegacyStore(userWalletId), + ) + } + private suspend fun getFromLegacyStore(userWalletId: UserWalletId): UserTokensResponse? { return userTokensResponseStore.getSyncOrNull(userWalletId) ?.let { diff --git a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt index 5af36f3594..6dc1771d27 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt @@ -102,12 +102,16 @@ internal class DefaultAccountsCRUDRepository( } override suspend fun saveAccounts(accountList: AccountList) { - val userWallet = userWalletsStore.getSyncStrict(accountList.userWalletId) + val converter = convertersContainer.createCryptoPortfolioConverter(userWalletId = accountList.userWalletId) - val converter = convertersContainer.getWalletAccountsResponseCF.create(userWallet = userWallet) - val accountsResponse = converter.convert(value = accountList) + val accountDTOs = converter.convertListBack( + input = accountList.accounts.filterIsInstance(), + ) - walletAccountsSaver.pushAndStore(userWalletId = userWallet.walletId, response = accountsResponse) + val syncedResponse = walletAccountsSaver.push(userWalletId = accountList.userWalletId, accounts = accountDTOs) + if (syncedResponse != null) { + walletAccountsSaver.store(userWalletId = accountList.userWalletId, response = syncedResponse) + } } override suspend fun saveAccount(account: Account.CryptoPortfolio) { diff --git a/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcherTest.kt b/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcherTest.kt index 4494f5af61..55be8a64db 100644 --- a/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcherTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcherTest.kt @@ -117,7 +117,7 @@ class DefaultWalletAccountsFetcherTest { eTag = eTag, body = SaveWalletAccountsResponse(updatedAccountsResponse.accounts), ) - } returns ApiResponse.Success(data = Unit) + } returns ApiResponse.Success(data = updatedAccountsResponse) // Act fetcher.fetch(userWalletId) @@ -273,26 +273,26 @@ class DefaultWalletAccountsFetcherTest { @Test fun `push should call saveWalletAccounts with correct params`() = runTest { // Arrange - val accounts = listOf(createWalletAccountDTO(userWalletId = userWalletId, tokens = null)) - val response = SaveWalletAccountsResponse(accounts) + val getResponse = createGetWalletAccountsResponse(userWalletId, tokens = null) + val saveResponse = SaveWalletAccountsResponse(getResponse.accounts) coEvery { tangemTechApi.saveWalletAccounts( walletId = userWalletId.stringValue, eTag = eTag, - body = response, + body = saveResponse, ) - } returns ApiResponse.Success(data = Unit) + } returns ApiResponse.Success(data = getResponse) // Act - fetcher.push(userWalletId, response) + fetcher.push(userWalletId, saveResponse) // Assert coVerify { tangemTechApi.saveWalletAccounts( walletId = userWalletId.stringValue, eTag = eTag, - body = response, + body = saveResponse, ) } } @@ -316,7 +316,7 @@ class DefaultWalletAccountsFetcherTest { eTag = eTag, body = response, ) - } returns saveApiResponse as ApiResponse + } returns saveApiResponse as ApiResponse // Act val actual = runCatching { fetcher.push(userWalletId, response) }.exceptionOrNull()!! @@ -326,42 +326,6 @@ class DefaultWalletAccountsFetcherTest { } } - @Nested - @TestInstance(TestInstance.Lifecycle.PER_CLASS) - inner class PushAndStore { - - @Test - fun `pushAndStore should call push and store with correct params`() = runTest { - // Arrange - val accounts = listOf(createWalletAccountDTO(userWalletId = userWalletId, tokens = null)) - val response = createGetWalletAccountsResponse(userWalletId).copy(accounts = accounts) - - coEvery { - tangemTechApi.saveWalletAccounts( - walletId = userWalletId.stringValue, - eTag = eTag, - body = SaveWalletAccountsResponse(accounts = response.accounts), - ) - } returns ApiResponse.Success(data = Unit) - - coEvery { accountsResponseStore.updateData(any()) } returns mockk() - - // Act - fetcher.pushAndStore(userWalletId, response) - - // Assert - coVerifyOrder { - tangemTechApi.saveWalletAccounts( - walletId = userWalletId.stringValue, - eTag = eTag, - body = SaveWalletAccountsResponse(accounts = response.accounts), - ) - accountsResponseStoreFactory.create(userWalletId) - accountsResponseStore.updateData(any()) - } - } - } - private fun createToken( networkId: String = "ethereum", derivationPath: String = "m/44'/60'/0'/0/0", diff --git a/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt b/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt index e8a3341536..6500556de8 100644 --- a/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt @@ -1,5 +1,7 @@ package com.tangem.data.account.fetcher +import com.tangem.data.account.converter.createGetWalletAccountsResponse +import com.tangem.data.account.converter.createWalletAccountDTO import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory import com.tangem.data.account.utils.toUserTokensResponse import com.tangem.data.common.currency.UserTokensSaver @@ -35,6 +37,10 @@ class FetchWalletAccountsErrorHandlerTest { defaultWalletAccountsResponseFactory = defaultWalletAccountsResponseFactory, ) + private val pushWalletAccounts: suspend (UserWalletId, List) -> GetWalletAccountsResponse = + mockk(relaxed = true) + private val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk(relaxed = true) + @BeforeEach fun setupEach() { clearMocks( @@ -53,9 +59,6 @@ class FetchWalletAccountsErrorHandlerTest { errorBody = null, ) - val pushWalletAccounts: suspend (UserWalletId, List) -> Unit = mockk() - val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk() - // Act handler.handle( error = error, @@ -84,29 +87,11 @@ class FetchWalletAccountsErrorHandlerTest { errorBody = null, ) - val accountDTO = WalletAccountDTO( - id = "nibh", - name = "Michael Dotson", - derivationIndex = 7135, - icon = "consectetuer", - iconColor = "ferri", - tokens = listOf(), - totalTokens = 7738, - totalNetworks = 3348, - ) + val accountDTO = createWalletAccountDTO(userWalletId) - val savedAccountsResponse = GetWalletAccountsResponse( - wallet = GetWalletAccountsResponse.Wallet( - group = UserTokensResponse.GroupType.NONE, - sort = UserTokensResponse.SortType.MANUAL, - totalAccounts = 1, - ), - accounts = listOf(accountDTO), - unassignedTokens = emptyList(), - ) + val savedAccountsResponse = createGetWalletAccountsResponse(userWalletId) - val pushWalletAccounts: suspend (UserWalletId, List) -> Unit = mockk(relaxed = true) - val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk(relaxed = true) + coEvery { pushWalletAccounts(userWalletId, listOf(accountDTO)) } returns savedAccountsResponse // Act handler.handle( @@ -119,8 +104,8 @@ class FetchWalletAccountsErrorHandlerTest { // Assert coVerify { - pushWalletAccounts(userWalletId, listOf(accountDTO)) userTokensSaver.push(userWalletId, response = savedAccountsResponse.toUserTokensResponse()) + pushWalletAccounts(userWalletId, listOf(accountDTO)) storeWalletAccounts(userWalletId, savedAccountsResponse) } @@ -163,9 +148,6 @@ class FetchWalletAccountsErrorHandlerTest { defaultWalletAccountsResponseFactory.create(userWalletId, userTokensResponse) } returns savedAccountsResponse - val pushWalletAccounts: suspend (UserWalletId, List) -> Unit = mockk(relaxed = true) - val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk(relaxed = true) - // Act handler.handle( error = error, diff --git a/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt b/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt index b830678839..9507b208c1 100644 --- a/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt @@ -25,7 +25,6 @@ import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountName import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.account.DerivationIndex -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* @@ -37,6 +36,7 @@ import kotlin.time.Duration.Companion.minutes /** [REDACTED_AUTHOR] */ +@Suppress("UnusedFlow") @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DefaultAccountsCRUDRepositoryTest { @@ -580,24 +580,18 @@ class DefaultAccountsCRUDRepositoryTest { @Test fun `saveAccounts should call API and update store`() = runTest { // Arrange - val userWallet = mockk { - every { this@mockk.walletId } returns userWalletId - } - val accountList = AccountList.empty(userWalletId = userWalletId) + val accounts = accountList.accounts.filterIsInstance() - val accountsResponse = mockk() + val accountsResponse = createGetWalletAccountsResponse(userWalletId) accountsResponseStoreFlow.value = accountsResponse - val converter = mockk { - every { this@mockk.convert(accountList) } returns accountsResponse + val converter = mockk { + every { this@mockk.convertListBack(accounts) } returns accountsResponse.accounts } - every { userWalletsStore.getSyncStrict(userWalletId) } returns userWallet - - every { - convertersContainer.getWalletAccountsResponseCF.create(userWallet = userWallet) - } returns converter + every { convertersContainer.createCryptoPortfolioConverter(userWalletId) } returns converter + coEvery { walletAccountsSaver.push(userWalletId, accountsResponse.accounts) } returns accountsResponse // Act repository.saveAccounts(accountList) @@ -606,37 +600,30 @@ class DefaultAccountsCRUDRepositoryTest { Truth.assertThat(accountsResponseStoreFlow.value).isEqualTo(accountsResponse) coVerifyOrder { - convertersContainer.getWalletAccountsResponseCF.create(userWallet) - converter.convert(accountList) - walletAccountsSaver.pushAndStore(userWalletId, accountsResponse) + convertersContainer.createCryptoPortfolioConverter(userWalletId) + converter.convertListBack(accounts) + walletAccountsSaver.push(userWalletId, accountsResponse.accounts) } } @Test fun `saveAccounts if API request is failed`() = runTest { // Arrange - val userWallet = mockk { - every { this@mockk.walletId } returns userWalletId - } - val accountList = AccountList.empty(userWalletId = userWalletId) + val accounts = accountList.accounts.filterIsInstance() - val accountsResponse = mockk() + val accountsResponse = createGetWalletAccountsResponse(userWalletId) accountsResponseStoreFlow.value = accountsResponse - val converter = mockk { - every { this@mockk.convert(accountList) } returns accountsResponse + val converter = mockk { + every { this@mockk.convertListBack(accounts) } returns accountsResponse.accounts } - every { userWalletsStore.getSyncStrict(userWalletId) } returns userWallet - - every { - convertersContainer.getWalletAccountsResponseCF.create(userWallet = userWallet) - } returns converter + every { convertersContainer.createCryptoPortfolioConverter(userWalletId) } returns converter val exception = Exception("Test error") - coEvery { walletAccountsSaver.pushAndStore(userWalletId, accountsResponse) } throws exception + coEvery { walletAccountsSaver.push(userWalletId, accountsResponse.accounts) } throws exception // Act val actual = runCatching { repository.saveAccounts(accountList) }.exceptionOrNull()!! @@ -646,9 +633,8 @@ class DefaultAccountsCRUDRepositoryTest { Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message) coVerifyOrder { - convertersContainer.getWalletAccountsResponseCF.create(userWallet) - converter.convert(accountList) - walletAccountsSaver.pushAndStore(userWalletId, accountsResponse) + convertersContainer.createCryptoPortfolioConverter(userWalletId) + converter.convertListBack(accounts) } } } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/account/WalletAccountsSaver.kt b/data/common/src/main/kotlin/com/tangem/data/common/account/WalletAccountsSaver.kt index f4b684613b..de6c29ccc4 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/account/WalletAccountsSaver.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/account/WalletAccountsSaver.kt @@ -12,18 +12,14 @@ import com.tangem.domain.models.wallet.UserWalletId */ interface WalletAccountsSaver { - /** Push and store wallet accounts [response] by [userWalletId] */ - @Throws - suspend fun pushAndStore(userWalletId: UserWalletId, response: GetWalletAccountsResponse) - /** Store wallet accounts [response] by [userWalletId] */ suspend fun store(userWalletId: UserWalletId, response: GetWalletAccountsResponse) /** Push wallet accounts [body] by [userWalletId] */ @Throws - suspend fun push(userWalletId: UserWalletId, body: SaveWalletAccountsResponse) + suspend fun push(userWalletId: UserWalletId, body: SaveWalletAccountsResponse): GetWalletAccountsResponse? /** Push wallet accounts [accounts] by [userWalletId] */ @Throws - suspend fun push(userWalletId: UserWalletId, accounts: List) + suspend fun push(userWalletId: UserWalletId, accounts: List): GetWalletAccountsResponse? } \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt index 1bff615daf..8e52598bb2 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt @@ -5,9 +5,11 @@ import arrow.core.getOrElse import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either +import arrow.core.raise.ensure import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.ArchivedAccount import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.account.tokens.MainAccountTokensMigration import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWalletId @@ -16,11 +18,13 @@ import com.tangem.domain.models.wallet.UserWalletId * Use case for recovering a crypto portfolio account from archived accounts * * @property crudRepository repository for performing CRUD operations on accounts + * @property mainAccountTokensMigration handles the migration of tokens from the main account to the recovered account * [REDACTED_AUTHOR] */ class RecoverCryptoPortfolioUseCase( private val crudRepository: AccountsCRUDRepository, + private val mainAccountTokensMigration: MainAccountTokensMigration, ) { /** @@ -30,15 +34,25 @@ class RecoverCryptoPortfolioUseCase( */ suspend operator fun invoke(accountId: AccountId): Either = either { val accountList = getAccountList(userWalletId = accountId.userWalletId) + + ensure(accountList.canAddMoreAccounts) { + raise(Error.AccountListRequirementsNotMet(cause = AccountList.Error.ExceedsMaxAccountsCount)) + } + val archivedAccount = getArchivedAccount(accountId = accountId) val recoveredAccount = archivedAccount.recover() val updatedAccountList = (accountList + recoveredAccount) - .getOrElse { raise(Error.CriticalTechError.AccountListRequirementsNotMet(cause = it)) } + .getOrElse { raise(Error.AccountListRequirementsNotMet(cause = it)) } saveAccounts(updatedAccountList) + mainAccountTokensMigration.migrate( + userWalletId = accountId.userWalletId, + derivationIndex = recoveredAccount.derivationIndex, + ) + recoveredAccount } @@ -47,7 +61,9 @@ class RecoverCryptoPortfolioUseCase( block = { crudRepository.getAccountListSync(userWalletId = userWalletId) }, catch = { raise(Error.DataOperationFailed(cause = it)) }, ) - .getOrElse { raise(Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId)) } + .getOrElse { + raise(Error.DataOperationFailed(message = "Account list not found for wallet $userWalletId")) + } } private suspend fun Raise.getArchivedAccount(accountId: AccountId): ArchivedAccount { @@ -56,7 +72,7 @@ class RecoverCryptoPortfolioUseCase( catch = { raise(Error.DataOperationFailed(cause = it)) }, ) .getOrElse { - raise(Error.CriticalTechError.AccountNotFound(accountId = accountId)) + raise(Error.DataOperationFailed(message = "Account not found: $accountId")) } } @@ -66,7 +82,6 @@ class RecoverCryptoPortfolioUseCase( accountName = this.name, icon = this.icon, derivationIndex = this.derivationIndex, - // TODO: [REDACTED_JIRA] cryptoCurrencies = emptySet(), ) } @@ -87,37 +102,18 @@ class RecoverCryptoPortfolioUseCase( get() = this::class.simpleName ?: "RecoverCryptoPortfolioUseCase.Error" /** - * Critical technical errors that can occur during the recovery operation + * Error indicating that the account list requirements were not met. + * + * @property cause the underlying cause of the error */ - sealed interface CriticalTechError : Error { - - /** - - * - * @property userWalletId the unique identifier of the user wallet - */ - data class AccountsNotCreated(val userWalletId: UserWalletId) : CriticalTechError { - override fun toString(): String = "$tag: Accounts for $userWalletId are not created" - } - - /** Error indicating that the account with [accountId] was not found */ - data class AccountNotFound(val accountId: AccountId) : CriticalTechError { - override fun toString(): String = "$tag: Account with ID $accountId not found" - } - - /** - * Error indicating that the account list requirements were not met. - * - * @property cause the underlying cause of the error - */ - data class AccountListRequirementsNotMet(val cause: AccountList.Error) : Error { - override fun toString(): String = "$tag: Account list requirements not met: $cause" - } + data class AccountListRequirementsNotMet(val cause: AccountList.Error) : Error { + override fun toString(): String = "$tag: Account list requirements not met: $cause" } /** Error indicating that a data operation failed */ data class DataOperationFailed(val cause: Throwable) : Error { - override fun toString(): String = "$tag: Data operation failed: ${cause.message ?: "Unknown error"}" + + constructor(message: String) : this(cause = IllegalStateException(message)) } } } \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt index f5ede48207..8a16fd034c 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt @@ -8,6 +8,7 @@ import com.google.common.truth.Truth import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.ArchivedAccount import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.account.tokens.MainAccountTokensMigration import com.tangem.domain.account.usecase.RecoverCryptoPortfolioUseCase.Error import com.tangem.domain.account.utils.createAccount import com.tangem.domain.models.account.AccountId @@ -26,7 +27,11 @@ import org.junit.jupiter.api.TestInstance class RecoverCryptoPortfolioUseCaseTest { private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) - private val useCase = RecoverCryptoPortfolioUseCase(crudRepository) + private val mainAccountTokensMigration: MainAccountTokensMigration = mockk() + private val useCase = RecoverCryptoPortfolioUseCase( + crudRepository = crudRepository, + mainAccountTokensMigration = mainAccountTokensMigration, + ) @BeforeEach fun resetMocks() { @@ -51,6 +56,7 @@ class RecoverCryptoPortfolioUseCaseTest { coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() coEvery { crudRepository.getArchivedAccountSync(account.accountId) } returns archivedAccount.toOption() + coEvery { mainAccountTokensMigration.migrate(userWalletId, account.derivationIndex) } returns Unit.right() // Act val actual = useCase(account.accountId) @@ -59,7 +65,7 @@ class RecoverCryptoPortfolioUseCaseTest { val expected = account.right() Truth.assertThat(actual).isEqualTo(expected) - coVerifyOrder { + coVerifySequence { crudRepository.getAccountListSync(userWalletId) crudRepository.getArchivedAccountSync(account.accountId) crudRepository.saveAccounts(updatedAccountList) @@ -77,13 +83,14 @@ class RecoverCryptoPortfolioUseCaseTest { coEvery { crudRepository.getAccountListSync(userWalletId) } returns None // Act - val actual = useCase(accountId) + val actual = useCase(accountId).leftOrNull() as Error.DataOperationFailed // Assert - val expected = Error.CriticalTechError.AccountsNotCreated(userWalletId).left() - Truth.assertThat(actual).isEqualTo(expected) + val expected = IllegalStateException("Account list not found for wallet $userWalletId") + Truth.assertThat(actual.cause).isInstanceOf(expected::class.java) + Truth.assertThat(actual.cause).hasMessageThat().isEqualTo(expected.message) - coVerifyOrder { crudRepository.getAccountListSync(userWalletId) } + coVerifySequence { crudRepository.getAccountListSync(userWalletId) } coVerify(inverse = true) { crudRepository.getArchivedAccountSync(any()) crudRepository.saveAccounts(any()) @@ -108,7 +115,7 @@ class RecoverCryptoPortfolioUseCaseTest { val expected = Error.DataOperationFailed(exception).left() Truth.assertThat(actual).isEqualTo(expected) - coVerifyOrder { crudRepository.getAccountListSync(userWalletId) } + coVerifySequence { crudRepository.getAccountListSync(userWalletId) } coVerify(inverse = true) { crudRepository.getArchivedAccountSync(any()) crudRepository.saveAccounts(any()) @@ -132,7 +139,7 @@ class RecoverCryptoPortfolioUseCaseTest { val expected = Error.DataOperationFailed(exception).left() Truth.assertThat(actual).isEqualTo(expected) - coVerifyOrder { + coVerifySequence { crudRepository.getAccountListSync(userWalletId) crudRepository.getArchivedAccountSync(account.accountId) } @@ -149,13 +156,14 @@ class RecoverCryptoPortfolioUseCaseTest { coEvery { crudRepository.getArchivedAccountSync(account.accountId) } returns None // Act - val actual = useCase(account.accountId) + val actual = useCase(account.accountId).leftOrNull() as Error.DataOperationFailed // Assert - val expected = Error.CriticalTechError.AccountNotFound(account.accountId).left() - Truth.assertThat(actual).isEqualTo(expected) + val expected = IllegalStateException("Account not found: ${account.accountId}") + Truth.assertThat(actual.cause).isInstanceOf(expected::class.java) + Truth.assertThat(actual.cause).hasMessageThat().isEqualTo(expected.message) - coVerifyOrder { + coVerifySequence { crudRepository.getAccountListSync(userWalletId) crudRepository.getArchivedAccountSync(account.accountId) } @@ -190,7 +198,7 @@ class RecoverCryptoPortfolioUseCaseTest { val expected = Error.DataOperationFailed(exception).left() Truth.assertThat(actual).isEqualTo(expected) - coVerifyOrder { + coVerifySequence { crudRepository.getAccountListSync(userWalletId) crudRepository.getArchivedAccountSync(account.accountId) crudRepository.saveAccounts(updatedAccountList) diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt index f1c4734092..76c6acb6da 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt @@ -1,6 +1,8 @@ package com.tangem.features.account.archived import com.tangem.common.ui.account.toUM +import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router @@ -11,6 +13,8 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.ToastMessage +import com.tangem.core.ui.utils.showErrorDialog +import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.ArchivedAccount import com.tangem.domain.account.usecase.ArchivedAccountList import com.tangem.domain.account.usecase.GetArchivedAccountsUseCase @@ -20,6 +24,7 @@ import com.tangem.domain.models.account.AccountId import com.tangem.features.account.ArchivedAccountListComponent import com.tangem.features.account.archived.entity.AccountArchivedUM import com.tangem.features.account.archived.entity.AccountArchivedUMBuilder +import com.tangem.features.account.createedit.error.AccountFeatureError import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn @@ -37,6 +42,7 @@ internal class ArchivedAccountListModel @Inject constructor( private val recoverCryptoPortfolioUseCase: RecoverCryptoPortfolioUseCase, private val getArchivedAccountsUseCase: GetArchivedAccountsUseCase, private val umBuilder: AccountArchivedUMBuilder, + private val analyticsExceptionHandler: AnalyticsExceptionHandler, ) : Model() { private val params = paramsContainer.require() @@ -103,11 +109,37 @@ internal class ArchivedAccountListModel @Inject constructor( ) } - private fun recoverCryptoPortfolio(accountId: AccountId) = modelScope.launch { + private fun recoverCryptoPortfolio(accountId: AccountId) = modelScope.launch(dispatchers.default) { recoverCryptoPortfolioUseCase(accountId) - .onLeft { Timber.e(it.toString()) } - .onRight { showSuccessRecoverMessage() } - router.pop() + .onLeft(::handleRecoverError) + .onRight { + showSuccessRecoverMessage() + router.pop() + } + } + + private fun handleRecoverError(error: RecoverCryptoPortfolioUseCase.Error) { + if (error is RecoverCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet && + error.cause is AccountList.Error.ExceedsMaxAccountsCount + ) { + // TODO("account") show alert that max accounts count reached + // https://www.figma.com/design/09KKG4ZVuFDZhj8WLv5rGJ/%F0%9F%9A%A7-App-experience?node-id=24765-180563&t=vk6TCy4MkYol1cPb-4 + return + } + + val featureError = AccountFeatureError.ArchivedAccountList.FailedToRecoverAccount(cause = error) + logError(error = featureError) + messageSender.showErrorDialog(universalError = featureError, onDismiss = router::pop) + } + + private fun logError(error: AccountFeatureError, params: Map = mapOf()) { + val exception = IllegalStateException(error.toString()) + + Timber.e(exception) + + analyticsExceptionHandler.sendException( + event = ExceptionAnalyticsEvent(exception = exception, params = params), + ) } private fun showSuccessRecoverMessage() {