Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-27 14:44:19 +03:00
parent f3f29813e0
commit b74e2b9168
25 changed files with 405 additions and 72 deletions

View file

@ -1,6 +1,7 @@
package com.tangem.data.appcurrency
import com.tangem.data.appcurrency.utils.AppCurrencyConverter
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
@ -15,7 +16,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.joda.time.Duration
import timber.log.Timber
internal class DefaultAppCurrencyRepository(
private val tangemTechApi: TangemTechApi,
@ -80,17 +80,15 @@ internal class DefaultAppCurrencyRepository(
}
private suspend fun fetchAvailableCurrencies() {
try {
val response = tangemTechApi.getCurrencyList()
val response = safeApiCall(
call = { tangemTechApi.getCurrencyList().bind() },
onError = {
cacheRegistry.invalidate(AVAILABLE_CURRENCIES_CACHE_KEY)
getDefaultCurrenciesResponse()
},
)
availableAppCurrenciesStore.store(response)
} catch (e: Throwable) {
Timber.e(e, "Unable to fetch available currencies")
availableAppCurrenciesStore.store(getDefaultCurrenciesResponse())
throw e
}
availableAppCurrenciesStore.store(response)
}
private fun getDefaultCurrenciesResponse(): CurrenciesResponse = CurrenciesResponse(

View file

@ -15,6 +15,7 @@ dependencies {
implementation(deps.kotlin.coroutines)
implementation(deps.jodatime)
implementation(deps.timber)
implementation(deps.arrow.core)
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)

View file

@ -0,0 +1,47 @@
package com.tangem.data.common.api
import arrow.core.raise.Raise
import arrow.core.raise.recover
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
import timber.log.Timber
/**
* A wrapper around the [Raise] interface specific for [ApiResponseError]. It provides utility functions to
* operate on [ApiResponse] instances.
*
* @property raise A [Raise] instance for raising [ApiResponseError].
*/
@JvmInline
value class ApiResponseRaise(
private val raise: Raise<ApiResponseError>,
) : Raise<ApiResponseError> by raise {
/**
* Binds the given [ApiResponse] to its underlying value or raises an error.
*
* @return The underlying data of the response if it's successful.
*/
fun <T : Any> ApiResponse<T>.bind(): T = when (this) {
is ApiResponse.Success -> data
is ApiResponse.Error -> raise.raise(cause)
}
}
/**
* Attempts to execute an API call safely, providing error handling.
*
* @param call The API call block to execute.
* @param onError A function to handle errors and return a fallback value of type [T].
*
* @return The result of the API call or the fallback value provided by [onError] if an error occurs.
*/
inline fun <T> safeApiCall(call: ApiResponseRaise.() -> T, onError: (ApiResponseError) -> T): T {
return recover(
block = { call(ApiResponseRaise(raise = this)) },
recover = {
Timber.w(it, "Unable to perform safe API call")
onError(it)
},
)
}

View file

@ -24,6 +24,15 @@ interface CacheRegistry {
*/
suspend fun invalidate(key: String)
/**
* Invalidates cache keys in registry.
*
* If the key doesn't exist, or it's already invalidated, this method doesn't have any effect.
*
* @param keys cache keys.
*/
suspend fun invalidate(keys: Collection<String>)
/**
* Invalidates all cache keys in the registry.
*

View file

@ -23,6 +23,11 @@ internal class DefaultCacheRegistry(
cacheKeysStore.remove(key)
}
override suspend fun invalidate(keys: Collection<String>) {
Timber.d("Invalidate cache keys: $keys")
cacheKeysStore.remove(keys)
}
override suspend fun invalidateAll() {
Timber.d("Invalidate all cache keys")
cacheKeysStore.clear()
@ -32,14 +37,14 @@ internal class DefaultCacheRegistry(
key: String,
skipCache: Boolean,
expireIn: Duration,
action: suspend () -> Unit,
block: suspend () -> Unit,
) {
val isExpired = isExpired(key) || skipCache
if (!isExpired) return
try {
Timber.d("Invoke the action associated with the cache key: $key")
action()
block()
} catch (e: Throwable) {
Timber.w(e, "The action related to the cache key has failed: $key")
throw e

View file

@ -1,8 +1,10 @@
package com.tangem.data.tokens.repository
import com.tangem.blockchain.common.Blockchain
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.tokens.utils.*
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.token.UserMarketCoinsStore
@ -21,10 +23,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import retrofit2.HttpException
import timber.log.Timber
import java.net.ConnectException
import java.net.UnknownHostException
internal class DefaultCurrenciesRepository(
private val tangemTechApi: TangemTechApi,
@ -259,14 +258,14 @@ internal class DefaultCurrenciesRepository(
private suspend fun fetchTokens(userWallet: UserWallet) {
val userWalletId = userWallet.walletId
val response = try {
with(tangemTechApi.getUserTokens(userWalletId.stringValue)) {
// The response may contain repeated tokens
copy(tokens = tokens.distinct())
}
} catch (e: Throwable) {
handleFetchTokensError(userWallet, e)
}
val response = safeApiCall(
call = {
tangemTechApi.getUserTokens(userWalletId.stringValue).bind().let {
it.copy(tokens = it.tokens.distinct())
}
},
onError = { handleFetchTokensError(userWallet, it) },
)
userTokensStore.store(userWallet.walletId, response)
fetchUserMarketCoinsByIds(userWalletId, response)
@ -288,7 +287,7 @@ internal class DefaultCurrenciesRepository(
}
}
private suspend fun handleFetchTokensError(userWallet: UserWallet, throwable: Throwable): UserTokensResponse {
private suspend fun handleFetchTokensError(userWallet: UserWallet, e: ApiResponseError): UserTokensResponse {
val userWalletId = userWallet.walletId
val response = userTokensStore.getSyncOrNull(userWalletId)
?: userTokensResponseFactory.createUserTokensResponse(
@ -297,27 +296,12 @@ internal class DefaultCurrenciesRepository(
isSortedByBalance = false,
)
when (throwable) {
is ConnectException,
is UnknownHostException,
-> {
Timber.e("Unable to fetch currencies due to lack of internet connection")
}
is HttpException -> {
if (throwable.code() == NOT_FOUND_HTTP_CODE) {
Timber.w(
throwable,
"Requested currencies could not be found in the remote store for: $userWalletId",
)
if (e is ApiResponseError.HttpException && e.code == ApiResponseError.HttpException.Code.NOT_FOUND) {
Timber.w(e, "Requested currencies could not be found in the remote store for: $userWalletId")
tangemTechApi.saveUserTokens(userWalletId.stringValue, response)
} else {
Timber.e(throwable, "Unable to fetch currencies for: $userWalletId")
}
}
else -> {
throw throwable
}
tangemTechApi.saveUserTokens(userWalletId.stringValue, response)
} else {
cacheRegistry.invalidate(getTokensCacheKey(userWalletId))
}
return response

View file

@ -1,5 +1,6 @@
package com.tangem.data.tokens.repository
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.tokens.utils.QuotesConverter
import com.tangem.datasource.api.tangemTech.TangemTechApi
@ -12,7 +13,6 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
internal class DefaultQuotesRepository(
private val tangemTechApi: TangemTechApi,
@ -72,15 +72,20 @@ internal class DefaultQuotesRepository(
}
private suspend fun fetchQuotes(rawCurrenciesIds: Set<String>, appCurrencyId: String) {
val response = try {
val coinIds = rawCurrenciesIds.joinToString(separator = ",")
tangemTechApi.getQuotes(appCurrencyId, coinIds)
} catch (e: Throwable) {
Timber.e(e, "Unable to fetch quotes for: $rawCurrenciesIds")
throw e
}
val response = safeApiCall(
call = {
val coinIds = rawCurrenciesIds.joinToString(separator = ",")
tangemTechApi.getQuotes(appCurrencyId, coinIds).bind()
},
onError = {
cacheRegistry.invalidate(rawCurrenciesIds.map(::getQuoteCacheKey))
null
},
)
quotesStore.store(response)
if (response != null) {
quotesStore.store(response)
}
}
private suspend fun filterExpiredCurrenciesIds(