Updated on 2026-08-14
This commit is contained in:
parent
275e07cb9d
commit
9fd43ad7fb
15 changed files with 56 additions and 502 deletions
|
|
@ -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/"
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue