Updated on 2026-08-14
This commit is contained in:
parent
ad0a6c81bf
commit
ae4c521813
10 changed files with 133 additions and 166 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue