Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-21 12:47:53 +04:00
parent 275e07cb9d
commit 9fd43ad7fb
15 changed files with 56 additions and 502 deletions

View file

@ -1,102 +0,0 @@
package com.tangem.tap.domain.tokens
import com.squareup.moshi.JsonClass
import com.tangem.blockchain.common.Blockchain
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.domain.common.extensions.fromNetworkId
@JsonClass(generateAdapter = true)
data class CurrencyFromJson(
val id: String,
val name: String,
val symbol: String,
val networks: List<ContractFromJson>? = null,
)
@JsonClass(generateAdapter = true)
data class ContractFromJson(
val networkId: String,
val contractAddress: String?,
val decimalCount: Int?,
)
@JsonClass(generateAdapter = true)
data class CurrenciesFromJson(
val imageHost: String?,
val coins: List<CurrencyFromJson>,
)
fun List<ContractFromJson>.toContracts(): List<Contract> {
return mapNotNull { Contract.fromJsonObject(it) }
}
data class Currency(
val id: String,
val name: String,
val symbol: String,
val iconUrl: String,
val contracts: List<Contract>,
) {
companion object {
fun fromJsonObject(currency: CurrencyFromJson): Currency {
return Currency(
id = currency.id,
name = currency.name,
symbol = currency.symbol,
iconUrl = getIconUrl(currency.id, null),
contracts = currency.networks?.toContracts() ?: emptyList(),
)
}
fun fromCoinResponse(currency: CoinsResponse.Coin, imageHost: String?): Currency {
return Currency(
id = currency.id,
name = currency.name,
symbol = currency.symbol,
iconUrl = getIconUrl(currency.id, imageHost),
contracts = currency.networks.mapNotNull { Contract.fromNetwork(it, imageHost) },
)
}
}
}
data class Contract(
val networkId: String,
val blockchain: Blockchain,
val address: String?,
val decimalCount: Int?,
val iconUrl: String,
) {
companion object {
fun fromJsonObject(contract: ContractFromJson): Contract? {
val blockchain = Blockchain.fromNetworkId(contract.networkId) ?: return null
return Contract(
networkId = contract.networkId,
blockchain = blockchain,
address = contract.contractAddress,
decimalCount = contract.decimalCount,
iconUrl = getIconUrl(contract.networkId, null),
)
}
fun fromNetwork(contract: CoinsResponse.Coin.Network, imageHost: String?): Contract? {
val blockchain = Blockchain.fromNetworkId(contract.networkId) ?: return null
return Contract(
networkId = contract.networkId,
blockchain = blockchain,
address = contract.contractAddress,
decimalCount = contract.decimalCount?.toInt(),
iconUrl = getIconUrl(contract.networkId, imageHost),
)
}
}
}
fun getIconUrl(id: String, imageHost: String? = null): String {
return "${imageHost ?: DEFAULT_IMAGE_HOST}large/$id.png"
}
private const val DEFAULT_IMAGE_HOST =
"https://s3.eu-central-1.amazonaws.com/tangem.api/coins/"

View file

@ -1,146 +0,0 @@
package com.tangem.tap.domain.tokens
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.common.core.TangemSdkError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.TangemTechService
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.domain.common.BlockchainNetwork
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.userwallets.UserWalletIdBuilder
import com.tangem.tap.domain.tokens.converters.CurrencyConverter
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.toBlockchainNetworks
import com.tangem.tap.features.wallet.models.toCurrencies
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import timber.log.Timber
class UserTokensRepository(
private val storageService: UserTokensStorageService,
private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider,
private val networkConnectionManager: NetworkConnectionManager,
) {
suspend fun getUserTokens(card: CardDTO, derivationStyle: DerivationStyle?): List<Currency> =
withContext(dispatchers.io) {
val userWalletId = getUserWalletId(card) ?: return@withContext emptyList()
if (DemoHelper.isDemoCardId(card.cardId)) {
return@withContext loadTokensOffline(userWalletId = userWalletId).ifEmpty {
loadDemoCurrencies(
derivationStyle,
)
}
}
if (!networkConnectionManager.isOnline) return@withContext loadTokensOffline(userWalletId = userWalletId)
return@withContext remoteGetUserTokens(userWalletId = userWalletId)
}
suspend fun saveUserTokens(card: CardDTO, tokens: List<Currency>) = withContext(dispatchers.io) {
val userWalletId = getUserWalletId(card) ?: return@withContext
val userTokens = tokens.toUserTokensResponse()
remoteSaveUserTokens(userWalletId = userWalletId, userTokens = userTokens)
storageService.saveUserTokens(userWalletId = userWalletId, tokens = userTokens)
}
// FIXME: Move to data layer
suspend fun loadBlockchainsToDerive(card: CardDTO, derivationStyle: DerivationStyle?): List<BlockchainNetwork> =
withContext(dispatchers.io) {
val userWalletId = getUserWalletId(card) ?: return@withContext emptyList()
val blockchainNetworks = loadTokensOffline(userWalletId = userWalletId).toBlockchainNetworks()
if (DemoHelper.isDemoCardId(card.cardId)) {
return@withContext blockchainNetworks.ifEmpty(loadDemoCurrencies(derivationStyle)::toBlockchainNetworks)
}
return@withContext blockchainNetworks
}
private fun loadTokensOffline(userWalletId: String): List<Currency> {
return storageService.getUserTokens(userWalletId = userWalletId) ?: emptyList()
}
// FIXME: Move to user wallet config
private fun loadDemoCurrencies(derivationStyle: DerivationStyle?): List<Currency> {
return DemoHelper.config.demoBlockchains
.map { blockchain ->
BlockchainNetwork(
blockchain = blockchain,
derivationPath = blockchain.derivationPath(derivationStyle)?.rawPath,
tokens = emptyList(),
)
}
.flatMap(BlockchainNetwork::toCurrencies)
}
private fun List<Currency>.toUserTokensResponse(): UserTokensResponse {
return UserTokensResponse(
tokens = CurrencyConverter.convertList(input = this),
group = UserTokensResponse.GroupType.NONE,
sort = UserTokensResponse.SortType.MANUAL,
)
}
private suspend fun handleGetUserTokensFailure(userWalletId: String, error: Throwable): List<Currency> {
return when {
error is TangemSdkError.NetworkError && error.customMessage.contains(NOT_FOUND_HTTP_CODE) -> {
storageService
.getUserTokens(userWalletId)
?.also { remoteSaveUserTokens(userWalletId = userWalletId, userTokens = it.toUserTokensResponse()) }
?: emptyList()
}
else -> {
storageService.getUserTokens(userWalletId)?.distinct() ?: emptyList()
}
}
}
private suspend fun remoteGetUserTokens(userWalletId: String): List<Currency> {
return runCatching { tangemTechApi.getUserTokens(userWalletId) }
.fold(
onSuccess = { response ->
response.getOrThrow()
.also { storageService.saveUserTokens(userWalletId, it) }
.tokens
.mapNotNull(Currency.Companion::fromTokenResponse)
.distinct()
},
onFailure = { handleGetUserTokensFailure(userWalletId = userWalletId, error = it) },
)
}
private suspend fun remoteSaveUserTokens(userWalletId: String, userTokens: UserTokensResponse) {
// it can throw okhttp3.internal.http2.StreamResetException: stream was reset: INTERNAL_ERROR
// if the /user-tokens endpoint disabled
runCatching { tangemTechApi.saveUserTokens(userWalletId, userTokens) }
.onFailure { Timber.e(it) }
}
private fun getUserWalletId(card: CardDTO): String? = UserWalletIdBuilder.card(card).build()?.stringValue
companion object {
private const val NOT_FOUND_HTTP_CODE = "404"
// TODO("After adding DI") get dependencies by DI
fun init(
tangemTechService: TangemTechService,
networkConnectionManager: NetworkConnectionManager,
storageService: UserTokensStorageService,
): UserTokensRepository {
return UserTokensRepository(
storageService = storageService,
tangemTechApi = tangemTechService.api,
dispatchers = AppCoroutineDispatcherProvider(),
networkConnectionManager = networkConnectionManager,
)
}
}
}

View file

@ -1,38 +0,0 @@
package com.tangem.tap.domain.tokens
import android.content.Context
import com.squareup.moshi.JsonAdapter
import com.tangem.Log
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.files.AndroidFileReader
import com.tangem.datasource.files.FileReader
import com.tangem.tap.features.wallet.models.Currency
@Deprecated("Use [com.tangem.datasource.local.token.UserTokensStore] instead.")
class UserTokensStorageService(private val fileReader: FileReader) {
private val userTokensAdapter: JsonAdapter<UserTokensResponse> =
MoshiConverter.networkMoshi.adapter(UserTokensResponse::class.java)
fun getUserTokens(userWalletId: String): List<Currency>? {
return try {
val json = fileReader.readFile(getFileNameForUserTokens(userWalletId))
userTokensAdapter.fromJson(json)?.tokens?.mapNotNull { Currency.fromTokenResponse(it) }
} catch (exception: Exception) {
Log.error { exception.stackTraceToString() }
null
}
}
fun saveUserTokens(userWalletId: String, tokens: UserTokensResponse) {
val json = userTokensAdapter.toJson(tokens)
fileReader.rewriteFile(json, getFileNameForUserTokens(userWalletId))
}
companion object {
private const val FILE_NAME_PREFIX_USER_TOKENS = "user_tokens"
private fun getFileNameForUserTokens(userId: String): String = "${FILE_NAME_PREFIX_USER_TOKENS}_$userId"
fun init(context: Context) = UserTokensStorageService(AndroidFileReader(context))
}
}

View file

@ -1,20 +0,0 @@
package com.tangem.tap.domain.tokens.converters
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
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 [UserTokensResponse.Token] */
object CurrencyConverter : Converter<Currency, UserTokensResponse.Token> {
override fun convert(value: Currency) = UserTokensResponse.Token(
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,29 +0,0 @@
package com.tangem.tap.domain.tokens.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.blockchain.common.Blockchain
@Deprecated("The class is used only for migration from older versions of the app")
@JsonClass(generateAdapter = true)
data class BlockchainDao(
@Json(name = "key")
val name: String,
@Json(name = "testnet")
val isTestNet: Boolean,
) {
@Deprecated("The method is used only for migration from older versions of the app")
fun toBlockchain(): Blockchain {
val blockchain = Blockchain.values().find { it.name.lowercase() == name.lowercase() }
?: error("Blockchain is null")
return if (!isTestNet) blockchain else blockchain.getTestnetVersion() ?: error("Invalid BlockchainDao")
}
companion object {
@Deprecated("The method is used only for migration from older versions of the app")
fun fromBlockchain(blockchain: Blockchain): BlockchainDao {
val name = blockchain.name.removeSuffix("Testnet").lowercase()
return BlockchainDao(name, blockchain.isTestnet())
}
}
}

View file

@ -1,24 +0,0 @@
package com.tangem.tap.domain.tokens.models
import com.squareup.moshi.JsonClass
import com.tangem.blockchain.common.Blockchain
@JsonClass(generateAdapter = true)
data class ObsoleteTokenDao(
val name: String,
val symbol: String,
val contractAddress: String,
val decimalCount: Int,
val customIconUrl: String?,
) {
fun toTokenDao(blockchain: Blockchain): TokenDao {
return TokenDao(
name = name,
symbol = symbol,
contractAddress = contractAddress,
decimalCount = decimalCount,
blockchainDao = BlockchainDao.fromBlockchain(blockchain),
customIconUrl = customIconUrl,
)
}
}

View file

@ -1,29 +0,0 @@
package com.tangem.tap.domain.tokens.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.blockchain.common.Token
import com.tangem.tap.domain.extensions.setCustomIconUrl
@JsonClass(generateAdapter = true)
data class TokenDao(
val name: String,
val symbol: String,
val contractAddress: String,
val decimalCount: Int,
@Json(name = "blockchain")
val blockchainDao: BlockchainDao,
val customIconUrl: String? = null,
val type: String? = null,
) {
fun toToken(): Token {
return Token(
name = name,
symbol = symbol,
contractAddress = contractAddress,
decimals = decimalCount,
).apply {
customIconUrl?.let { this.setCustomIconUrl(it) }
}
}
}