Updated on 2026-08-14

This commit is contained in:
Tangem 2023-01-09 12:39:33 +04:00
parent ad0a6c81bf
commit ae4c521813
10 changed files with 133 additions and 166 deletions

View file

@ -2,7 +2,6 @@ package com.tangem.tap.domain
import androidx.annotation.StringRes
import com.tangem.common.core.TangemError
import com.tangem.datasource.api.tangemTech.TangemTechError
import com.tangem.wallet.R
interface TapErrors
@ -78,13 +77,4 @@ fun TapErrors.assembleErrors(): MutableList<Pair<Int, List<Any>?>> {
is TapError -> idList.add(Pair(this.messageResource, this.args))
}
return idList
}
fun TangemTechError.toTapError(): TapError {
return when (this.code) {
404 -> NoDataError(this.description)
else -> TapError.CustomError(customMessage = this.description)
}
}
class NoDataError(message: String) : TapError.CustomError(customMessage = message)
}

View file

@ -1,27 +0,0 @@
package com.tangem.tap.domain.tokens
import com.tangem.common.core.TangemSdkError
import com.tangem.common.services.Result
import com.tangem.datasource.api.tangemTech.TangemTechService
import com.tangem.datasource.api.tangemTech.UserTokensResponse
import com.tangem.tap.domain.NoDataError
class UserTokensNetworkService(private val tangemTechService: TangemTechService) {
suspend fun getUserTokens(userId: String): Result<UserTokensResponse> {
return when (val result = tangemTechService.getUserTokens(userId)) {
is Result.Success -> result
is Result.Failure -> {
val error = result.error
if (error is TangemSdkError.NetworkError && error.customMessage.contains("404")) {
return Result.Failure(NoDataError(error.customMessage))
} else {
return result
}
}
}
}
suspend fun saveUserTokens(userId: String, tokens: UserTokensResponse): Result<Unit> {
return tangemTechService.putUserTokens(userId, tokens)
}
}

View file

@ -2,13 +2,14 @@ package com.tangem.tap.domain.tokens
import android.content.Context
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.common.services.Result
import com.tangem.common.core.TangemSdkError
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.TangemTechService
import com.tangem.datasource.api.tangemTech.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.util.userWalletId
import com.tangem.tap.common.AndroidFileReader
import com.tangem.tap.domain.NoDataError
import com.tangem.tap.domain.tokens.converters.CurrencyConverter
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.wallet.models.Currency
@ -16,93 +17,91 @@ import com.tangem.tap.features.wallet.models.toBlockchainNetworks
import com.tangem.tap.features.wallet.models.toCurrencies
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.store
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
class UserTokensRepository(
private val storageService: UserTokensStorageService,
private val networkService: UserTokensNetworkService,
private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider,
) {
suspend fun getUserTokens(card: CardDTO): List<Currency> {
// TODO("After adding DI") replace with CoroutineDispatcherProvider
suspend fun getUserTokens(card: CardDTO): List<Currency> = withContext(dispatchers.io) {
val userId = card.userWalletId.stringValue
if (DemoHelper.isDemoCardId(card.cardId)) {
return loadTokensOffline(card, userId).ifEmpty { loadDemoCurrencies() }
return@withContext loadTokensOffline(card, userId).ifEmpty(::loadDemoCurrencies)
}
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
return loadTokensOffline(card, userId)
return@withContext loadTokensOffline(card, userId)
}
return when (val networkResult = networkService.getUserTokens(userId)) {
is Result.Success -> {
val tokens = networkResult.data.tokens.mapNotNull { Currency.fromTokenResponse(it) }
storageService.saveUserTokens(userId, tokens.toUserTokensResponse())
tokens.distinct()
runCatching { tangemTechApi.getUserTokens(userId) }
.onSuccess { response ->
return@withContext response.tokens
.mapNotNull(Currency.Companion::fromTokenResponse).also {
storageService.saveUserTokens(userId, it.toUserTokensResponse())
}
.distinct()
}
.onFailure {
return@withContext handleGetUserTokensFailure(card = card, userId = userId, error = it)
}
is Result.Failure -> {
handleGetUserTokensFailure(card = card, userId = userId, error = networkResult.error)
}
}
throw IllegalStateException("Unreachable code because runCatching must return result")
}
suspend fun saveUserTokens(card: CardDTO, tokens: List<Currency>) {
// TODO("After adding DI") replace with CoroutineDispatcherProvider
suspend fun saveUserTokens(card: CardDTO, tokens: List<Currency>) = withContext(dispatchers.io) {
val userId = card.userWalletId.stringValue
val userTokens = tokens.toUserTokensResponse()
networkService.saveUserTokens(userId, userTokens)
tangemTechApi.saveUserTokens(userId, userTokens)
storageService.saveUserTokens(userId, userTokens)
}
suspend fun removeUserTokens(card: CardDTO) {
val userId = card.userWalletId.stringValue
val userTokens = emptyList<Currency>().toUserTokensResponse()
networkService.saveUserTokens(userId, userTokens)
storageService.saveUserTokens(userId, userTokens)
}
private fun List<Currency>.toUserTokensResponse(): UserTokensResponse {
val tokensResponse = this.map { it.toTokenResponse() }
return UserTokensResponse(
tokens = tokensResponse,
group = GROUP_DEFAULT_VALUE,
sort = SORT_DEFAULT_VALUE,
)
}
suspend fun loadBlockchainsToDerive(card: CardDTO): List<BlockchainNetwork> {
val userId = card.userWalletId.stringValue
val blockchainNetworks = loadTokensOffline(card, userId).toBlockchainNetworks()
suspend fun loadBlockchainsToDerive(card: CardDTO): List<BlockchainNetwork> = withContext(dispatchers.io) {
val blockchainNetworks = loadTokensOffline(
card = card,
userId = card.userWalletId.stringValue,
).toBlockchainNetworks()
if (DemoHelper.isDemoCardId(card.cardId)) {
return blockchainNetworks
.ifEmpty { loadDemoCurrencies().toBlockchainNetworks() }
return@withContext blockchainNetworks.ifEmpty(loadDemoCurrencies()::toBlockchainNetworks)
}
return blockchainNetworks
return@withContext blockchainNetworks
}
private suspend fun loadTokensOffline(card: CardDTO, userId: String): List<Currency> {
return storageService.getUserTokens(userId) ?: storageService.getUserTokens(card)
}
private fun loadDemoCurrencies(): List<Currency> {
return DemoHelper.config.demoBlockchains.map {
BlockchainNetwork(
blockchain = it,
derivationPath = it.derivationPath(DerivationStyle.LEGACY)?.rawPath,
tokens = emptyList(),
)
}.flatMap { it.toCurrencies() }
return DemoHelper.config.demoBlockchains
.map { blockchain ->
BlockchainNetwork(
blockchain = blockchain,
derivationPath = blockchain.derivationPath(DerivationStyle.LEGACY)?.rawPath,
tokens = emptyList(),
)
}
.flatMap(BlockchainNetwork::toCurrencies)
}
private suspend fun handleGetUserTokensFailure(
card: CardDTO,
userId: String,
error: Throwable,
): List<Currency> {
return when (error) {
is NoDataError -> {
val tokens = storageService.getUserTokens(card)
val userTokens = tokens.toUserTokensResponse()
coroutineScope { launch { networkService.saveUserTokens(userId = userId, tokens = userTokens) } }
tokens
}
private fun List<Currency>.toUserTokensResponse() = UserTokensResponse(
tokens = CurrencyConverter.convertList(this),
group = GROUP_DEFAULT_VALUE,
sort = SORT_DEFAULT_VALUE,
)
private suspend fun handleGetUserTokensFailure(card: CardDTO, userId: String, error: Throwable): List<Currency> {
return when {
error is TangemSdkError.NetworkError && error.customMessage.contains(NOT_FOUND_HTTP_CODE) ->
storageService.getUserTokens(card).also {
tangemTechApi.saveUserTokens(userId = userId, userTokens = it.toUserTokensResponse())
}
else -> {
val tokens = storageService.getUserTokens(userId) ?: storageService.getUserTokens(card)
tokens.distinct()
@ -110,21 +109,28 @@ class UserTokensRepository(
}
}
private suspend fun loadTokensOffline(card: CardDTO, userId: String): List<Currency> {
return storageService.getUserTokens(userId) ?: storageService.getUserTokens(card)
}
companion object {
const val SORT_DEFAULT_VALUE = "manual"
const val GROUP_DEFAULT_VALUE = "none"
private const val GROUP_DEFAULT_VALUE = "none"
private const val SORT_DEFAULT_VALUE = "manual"
private const val NOT_FOUND_HTTP_CODE = "404"
// TODO("After adding DI") get dependencies by DI
fun init(context: Context, tangemTechService: TangemTechService): UserTokensRepository {
val fileReader = AndroidFileReader(context)
val oldUserTokensRepository = OldUserTokensRepository(
fileReader, store.state.domainNetworks.tangemTechService,
fileReader = fileReader,
tangemNetworkService = store.state.domainNetworks.tangemTechService,
)
val storageService = UserTokensStorageService(
oldUserTokensRepository = oldUserTokensRepository,
fileReader = fileReader,
)
return UserTokensRepository(
storageService = storageService,
tangemTechApi = tangemTechService.api,
dispatchers = AppCoroutineDispatcherProvider(),
)
val storageService = UserTokensStorageService(oldUserTokensRepository, fileReader)
val networkService = UserTokensNetworkService(tangemTechService)
return UserTokensRepository(storageService, networkService)
}
}
}

View file

@ -2,8 +2,8 @@ package com.tangem.tap.domain.tokens
import com.squareup.moshi.JsonAdapter
import com.tangem.Log
import com.tangem.datasource.api.tangemTech.UserTokensResponse
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.common.CardDTO
import com.tangem.tap.common.FileReader
import com.tangem.tap.features.wallet.models.Currency

View file

@ -0,0 +1,20 @@
package com.tangem.tap.domain.tokens.converters
import com.tangem.datasource.api.tangemTech.models.TokenBody
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.utils.converter.Converter
/** Converter from domain model [Currency] to data model [TokenBody] */
object CurrencyConverter : Converter<Currency, TokenBody> {
override fun convert(value: Currency) = TokenBody(
id = value.coinId,
networkId = value.blockchain.toNetworkId(),
derivationPath = value.derivationPath,
name = value.currencyName,
symbol = value.currencySymbol,
decimals = value.decimals,
contractAddress = if (value is Currency.Token) value.token.contractAddress else null
)
}

View file

@ -1,11 +1,10 @@
package com.tangem.tap.features.wallet.models
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.datasource.api.tangemTech.models.TokenBody
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.datasource.api.tangemTech.TokenResponse
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
@ -57,17 +56,6 @@ sealed interface Currency {
fun isBlockchain(): Boolean = this is Blockchain
fun isToken(): Boolean = this is Token
fun toTokenResponse(): TokenResponse {
return TokenResponse(
id = coinId,
networkId = blockchain.toNetworkId(),
derivationPath = derivationPath,
name = currencyName,
symbol = currencySymbol,
decimals = decimals,
contractAddress = if (this is Token) token.contractAddress else null,
)
}
companion object {
fun fromBlockchainNetwork(
@ -110,24 +98,24 @@ sealed interface Currency {
)
}
fun fromTokenResponse(tokenResponse: TokenResponse): Currency? {
val blockchain = com.tangem.blockchain.common.Blockchain.fromNetworkId(tokenResponse.networkId)
fun fromTokenResponse(tokenBody: TokenBody): Currency? {
val blockchain = com.tangem.blockchain.common.Blockchain.fromNetworkId(tokenBody.networkId)
?: return null
return when {
tokenResponse.contractAddress != null -> Token(
tokenBody.contractAddress != null -> Token(
token = SdkToken(
name = tokenResponse.name,
symbol = tokenResponse.symbol,
contractAddress = tokenResponse.contractAddress!!,
decimals = tokenResponse.decimals,
id = tokenResponse.id,
name = tokenBody.name,
symbol = tokenBody.symbol,
contractAddress = tokenBody.contractAddress!!,
decimals = tokenBody.decimals,
id = tokenBody.id,
),
blockchain = blockchain,
derivationPath = tokenResponse.derivationPath,
derivationPath = tokenBody.derivationPath,
)
else -> Blockchain(
blockchain = blockchain,
derivationPath = tokenResponse.derivationPath,
derivationPath = tokenBody.derivationPath,
)
}
}

View file

@ -32,26 +32,4 @@ data class CoinsResponse(
}
//rates.keys = networkId's
data class RatesResponse(val rates: Map<String, Double>) : TangemTechResponse
data class UserTokensResponse(
val version: Int = 0,
val group: String? = null,
val sort: String? = null,
val tokens: List<TokenResponse> = emptyList(),
) : TangemTechResponse
data class TokenResponse(
val id: String? = null,
val networkId: String,
val derivationPath: String? = null,
val name: String,
val symbol: String,
val decimals: Int,
val contractAddress: String?,
) : TangemTechResponse
data class TangemTechError(
val code: Int,
val description: String,
)
data class RatesResponse(val rates: Map<String, Double>) : TangemTechResponse

View file

@ -2,6 +2,7 @@ package com.tangem.datasource.api.tangemTech
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
import com.tangem.datasource.api.tangemTech.models.GeoResponse
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.PUT
@ -40,5 +41,5 @@ interface TangemTechApi {
suspend fun getUserTokens(@Path(value = "user-id") userId: String): UserTokensResponse
@PUT("user-tokens/{user-id}")
suspend fun putUserTokens(@Path(value = "user-id") userId: String, @Body userTokens: UserTokensResponse)
suspend fun saveUserTokens(@Path(value = "user-id") userId: String, @Body userTokens: UserTokensResponse)
}

View file

@ -50,15 +50,6 @@ class TangemTechService(
}
}
suspend fun getUserTokens(userId: String): Result<UserTokensResponse> = withContext(Dispatchers.IO) {
performRequest { api.getUserTokens(userId) }
}
suspend fun putUserTokens(userId: String, userTokens: UserTokensResponse): Result<Unit> =
withContext(Dispatchers.IO) {
performRequest { api.putUserTokens(userId, userTokens) }
}
fun addHeaderInterceptors(interceptors: List<AddHeaderInterceptor>) {
headerInterceptors.removeAll(interceptors)
headerInterceptors.addAll(interceptors)

View file

@ -0,0 +1,20 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
data class UserTokensResponse(
@Json(name = "version") val version: Int = 0,
@Json(name = "group") val group: String? = null,
@Json(name = "sort") val sort: String? = null,
@Json(name = "tokens") val tokens: List<TokenBody> = emptyList(),
)
data class TokenBody(
@Json(name = "id") val id: String? = null,
@Json(name = "networkId") val networkId: String,
@Json(name = "derivationPath") val derivationPath: String? = null,
@Json(name = "name") val name: String,
@Json(name = "symbol") val symbol: String,
@Json(name = "decimals") val decimals: Int,
@Json(name = "contractAddress") val contractAddress: String?,
)