Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-31 10:00:15 +04:00
parent e7ebbb931e
commit 507b608807
12 changed files with 152 additions and 37 deletions

View file

@ -1,35 +1,50 @@
package com.tangem.data.account.fetcher
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
import com.tangem.data.common.cache.etag.ETagsStore
import com.tangem.data.common.currency.UserTokensResponseAccountIdEnricher
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code
import com.tangem.datasource.api.common.response.ETAG_HEADER
import com.tangem.datasource.api.common.response.isNetworkError
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.OnlyWalletIdBody
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
/**
* Handles errors that occur during the fetching of wallet accounts
*
* @property tangemTechApi API for network requests
* @property userTokensSaver saves user tokens to the storage
* @property userTokensResponseStore provides access to user token responses.
* @property defaultWalletAccountsResponseFactory creates [GetWalletAccountsResponse] from [UserTokensResponse]
* @property eTagsStore store for ETags to manage caching
* @property dispatchers dispatchers
*
* @see DefaultWalletAccountsFetcher
*
[REDACTED_AUTHOR]
*/
@Singleton
internal class FetchWalletAccountsErrorHandler @Inject constructor(
private val tangemTechApi: TangemTechApi,
private val userTokensSaver: UserTokensSaver,
private val userTokensResponseStore: UserTokensResponseStore,
private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory,
private val eTagsStore: ETagsStore,
private val dispatchers: CoroutineDispatcherProvider,
) {
/**
@ -58,16 +73,18 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor(
}
}
var response = savedAccountsResponse ?: createDefaultResponse(userWalletId)
val response = savedAccountsResponse ?: createDefaultResponse(userWalletId)
val (accountDTOs, userTokensResponse) = response.accounts to response.toUserTokensResponse()
val isNotFoundError = error.isNetworkError(code = Code.NOT_FOUND)
if (isNotFoundError) {
userTokensSaver.push(userWalletId = userWalletId, response = userTokensResponse)
val updatedResponse = pushWalletAccounts(userWalletId, accountDTOs)
val eTag = createWallet(userWalletId)
if (updatedResponse != null) {
response = updatedResponse
if (eTag != null) {
eTagsStore.store(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts, value = eTag)
pushWalletAccounts(userWalletId, accountDTOs)
userTokensSaver.push(userWalletId, userTokensResponse)
}
}
@ -92,4 +109,24 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor(
}
.also { userTokensResponseStore.clear(userWalletId) }
}
/**
* Creates a wallet on the server and returns the ETag if successful.
*
* @param userWalletId The ID of the user wallet to create.
*/
private suspend fun createWallet(userWalletId: UserWalletId): String? {
val creationResponse = withContext(dispatchers.io) {
tangemTechApi.createWallet(
body = OnlyWalletIdBody(walletId = userWalletId.stringValue),
)
}
return if (creationResponse is ApiResponse.Success && creationResponse.code == Code.CREATED) {
creationResponse.headers[ETAG_HEADER]?.firstOrNull()
} else {
null
}
}
}

View file

@ -3,15 +3,21 @@ 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.common.cache.etag.ETagsStore
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code
import com.tangem.datasource.api.common.response.ETAG_HEADER
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.OnlyWalletIdBody
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.coVerify
@ -27,14 +33,19 @@ import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class FetchWalletAccountsErrorHandlerTest {
private val tangemTechApi: TangemTechApi = mockk()
private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true)
private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true)
private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory = mockk()
private val eTagsStore: ETagsStore = mockk(relaxUnitFun = true)
private val handler = FetchWalletAccountsErrorHandler(
tangemTechApi = tangemTechApi,
userTokensSaver = userTokensSaver,
userTokensResponseStore = userTokensResponseStore,
defaultWalletAccountsResponseFactory = defaultWalletAccountsResponseFactory,
eTagsStore = eTagsStore,
dispatchers = TestingCoroutineDispatcherProvider(),
)
private val pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> GetWalletAccountsResponse =
@ -93,6 +104,16 @@ class FetchWalletAccountsErrorHandlerTest {
val savedAccountsResponse = createGetWalletAccountsResponse(userWalletId)
val eTagValue = "etag-value"
val apiResponse = ApiResponse.Success(
data = Unit,
headers = mapOf(ETAG_HEADER to listOf(eTagValue)),
code = Code.CREATED,
)
coEvery {
tangemTechApi.createWallet(OnlyWalletIdBody(userWalletId.stringValue))
} returns apiResponse
coEvery { pushWalletAccounts(userWalletId, listOf(accountDTO)) } returns savedAccountsResponse
// Act
@ -107,6 +128,8 @@ class FetchWalletAccountsErrorHandlerTest {
// Assert
coVerify {
userTokensSaver.push(userWalletId, response = savedAccountsResponse.toUserTokensResponse())
tangemTechApi.createWallet(OnlyWalletIdBody(userWalletId.stringValue))
eTagsStore.store(userWalletId, ETagsStore.Key.WalletAccounts, eTagValue)
pushWalletAccounts(userWalletId, listOf(accountDTO))
storeWalletAccounts(userWalletId, savedAccountsResponse)
}