Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-03 12:24:11 +03:00
parent dfa6798f07
commit 5883823eb3
71 changed files with 221 additions and 285 deletions

View file

@ -18,7 +18,7 @@ class CryptoCurrencyFactory(
@Suppress("LongParameterList") // Yep, it's long
fun createToken(
network: Network,
rawId: String?,
rawId: CryptoCurrency.RawID?,
name: String,
symbol: String,
decimals: Int,
@ -125,7 +125,7 @@ class CryptoCurrencyFactory(
symbol = cryptoCurrency.symbol,
contractAddress = cryptoCurrency.contractAddress,
decimals = cryptoCurrency.decimals,
id = cryptoCurrency.id.rawCurrencyId,
id = cryptoCurrency.id.rawCurrencyId?.value,
)
val blockchain = Blockchain.fromNetworkId(cryptoCurrency.network.backendId) ?: Blockchain.Unknown
val id = getTokenId(network, sdkToken)

View file

@ -4,6 +4,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.IconsUtil
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrency.ID
import com.tangem.domain.tokens.model.Network
import com.tangem.blockchain.common.Token as SdkToken
@ -31,15 +32,16 @@ fun getCoinId(network: Network, coinId: String): ID {
fun getTokenId(network: Network, sdkToken: SdkToken): ID {
val sdkTokenId = sdkToken.id
val tokenId = sdkTokenId?.let { CryptoCurrency.RawID(it) }
return getTokenId(network, sdkTokenId, sdkToken.contractAddress)
return getTokenId(network, tokenId, sdkToken.contractAddress)
}
fun getTokenId(network: Network, rawTokenId: String?, contractAddress: String): ID {
fun getTokenId(network: Network, rawTokenId: CryptoCurrency.RawID?, contractAddress: String): ID {
val suffix = if (rawTokenId == null) {
CustomCurrencyIdSuffix(contractAddress)
} else {
CurrencyIdSuffix(rawTokenId, contractAddress)
CurrencyIdSuffix(rawTokenId.value, contractAddress)
}
return ID(TOKEN_ID_PREFIX, getCurrencyIdBody(network), suffix)
@ -47,11 +49,12 @@ fun getTokenId(network: Network, rawTokenId: String?, contractAddress: String):
fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? {
val tokenId = token.id
val rawID = tokenId?.let { CryptoCurrency.RawID(it) }
return if (tokenId == null) {
return if (rawID == null) {
IconsUtil.getTokenIconUri(blockchain, token)?.toString()
} else {
getTokenIconUrlFromDefaultHost(tokenId)
getTokenIconUrlFromDefaultHost(rawID)
}
}
@ -59,7 +62,7 @@ fun getCoinIconUrl(blockchain: Blockchain): String? {
val coinId = when (blockchain) {
Blockchain.Unknown -> null
else -> blockchain.toCoinId()
}
}?.let { CryptoCurrency.RawID(it) }
return coinId?.let(::getTokenIconUrlFromDefaultHost)
}
@ -84,13 +87,13 @@ private fun getCurrencyIdBody(network: Network): CurrencyIdBody {
}
}
fun getTokenIconUrlFromDefaultHost(tokenId: String): String {
fun getTokenIconUrlFromDefaultHost(tokenId: CryptoCurrency.RawID): String {
return buildString {
append(DEFAULT_TOKENS_ICONS_HOST)
append('/')
append(TOKEN_ICON_SIZE)
append('/')
append(tokenId)
append(tokenId.value)
append('.')
append(TOKEN_ICON_EXT)
}

View file

@ -30,7 +30,7 @@ class UserTokensResponseFactory {
val blockchain = getBlockchain(currency.network.id)
return UserTokensResponse.Token(
id = currency.id.rawCurrencyId,
id = currency.id.rawCurrencyId?.value,
networkId = blockchain.toNetworkId(),
derivationPath = currency.network.derivationPath.value,
name = currency.name,

View file

@ -122,7 +122,7 @@ internal class DefaultCustomTokensRepository(
if (coinNetwork != null) {
cryptoCurrencyFactory.createToken(
network = network,
rawId = coin.id,
rawId = CryptoCurrency.RawID(coin.id),
name = coin.name,
symbol = coin.symbol,
decimals = coinNetwork.decimalCount!!.toInt(),
@ -154,7 +154,7 @@ internal class DefaultCustomTokensRepository(
override suspend fun createToken(
managedCryptoCurrency: ManagedCryptoCurrency.Token,
sourceNetwork: ManagedCryptoCurrency.SourceNetwork.Default,
rawId: String?,
rawId: CryptoCurrency.RawID?,
): CryptoCurrency.Token {
return cryptoCurrencyFactory.createToken(
network = sourceNetwork.network,

View file

@ -20,6 +20,7 @@ import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
internal class ManagedCryptoCurrencyFactory(
@ -114,7 +115,11 @@ internal class ManagedCryptoCurrencyFactory(
)
} else {
ManagedCryptoCurrency.Custom.Token(
currencyId = getTokenId(network, token.id, contractAddress),
currencyId = getTokenId(
network = network,
rawTokenId = token.id?.let { CryptoCurrency.RawID(it) },
contractAddress = contractAddress,
),
name = token.name,
symbol = token.symbol,
iconUrl = token.id?.let { getIconUrl(it) },

View file

@ -24,6 +24,7 @@ dependencies {
implementation(projects.domain.markets)
implementation(projects.domain.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.tokens)
implementation(projects.data.common)

View file

@ -125,10 +125,10 @@ internal class DefaultMarketsTokenRepository(
override suspend fun getChart(
fiatCurrencyCode: String,
interval: PriceChangeInterval,
tokenId: String,
tokenId: CryptoCurrency.RawID,
tokenSymbol: String,
) = withContext(dispatcherProvider.io) {
val mappedTokenId = getTokenIdIfL2Network(tokenId)
val mappedTokenId = getTokenIdIfL2Network(tokenId.value)
val response = marketsApi.getCoinChart(
currency = fiatCurrencyCode,
coinId = mappedTokenId,
@ -161,10 +161,10 @@ internal class DefaultMarketsTokenRepository(
override suspend fun getChartPreview(
fiatCurrencyCode: String,
interval: PriceChangeInterval,
tokenId: String,
tokenId: CryptoCurrency.RawID,
tokenSymbol: String,
) = withContext(dispatcherProvider.io) {
val mappedTokenId = getTokenIdIfL2Network(tokenId)
val mappedTokenId = getTokenIdIfL2Network(tokenId.value)
val chart = catchListErrorAndSendEvent {
marketsApi.getCoinsListCharts(
@ -193,7 +193,7 @@ internal class DefaultMarketsTokenRepository(
override suspend fun getTokenInfo(
fiatCurrencyCode: String,
tokenId: String,
tokenId: CryptoCurrency.RawID,
tokenSymbol: String,
languageCode: String,
) = withContext(dispatcherProvider.io) {
@ -203,16 +203,16 @@ internal class DefaultMarketsTokenRepository(
) {
marketsApi.getCoinMarketData(
currency = fiatCurrencyCode,
coinId = tokenId,
coinId = tokenId.value,
language = languageCode,
).getOrThrow()
}
val resultResponse = result.applyL2Compatibility(tokenId)
val resultResponse = result.applyL2Compatibility(tokenId.value)
return@withContext tokenMarketInfoConverter.convert(resultResponse)
}
override suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String, tokenSymbol: String) =
override suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: CryptoCurrency.RawID, tokenSymbol: String) =
withContext(dispatcherProvider.io) {
// for second markets iteration we should use extended api method with all required fields
@ -222,7 +222,7 @@ internal class DefaultMarketsTokenRepository(
) {
tangemTechApi.getQuotes(
currencyId = fiatCurrencyCode,
coinIds = tokenId,
coinIds = tokenId.value,
fields = marketsQuoteFields.joinToString(separator = ","),
).getOrThrow()
}
@ -263,10 +263,10 @@ internal class DefaultMarketsTokenRepository(
}
}
override suspend fun getTokenExchanges(tokenId: String): List<TokenMarketExchange> {
override suspend fun getTokenExchanges(tokenId: CryptoCurrency.RawID): List<TokenMarketExchange> {
return withContext(dispatcherProvider.io) {
cacheRegistry.invokeOnExpire(key = "coins/$tokenId/exchanges", skipCache = false) {
val response = marketsApi.getCoinExchanges(coinId = tokenId).getOrThrow()
val response = marketsApi.getCoinExchanges(coinId = tokenId.value).getOrThrow()
tokenExchangesStore.store(value = response.exchanges)
}

View file

@ -4,16 +4,17 @@ import com.tangem.datasource.api.markets.models.response.TokenMarketChartListRes
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenMarket
import com.tangem.domain.markets.TokenMarketListConfig
import com.tangem.domain.tokens.model.CryptoCurrency
internal object TokenMarketChartsConverter {
fun convert(
chartsToCopy: TokenMarket.Charts,
tokenId: String,
tokenId: CryptoCurrency.RawID,
interval: TokenMarketListConfig.Interval,
value: TokenMarketChartListResponse,
): TokenMarket.Charts {
val prices = requireNotNull(value[tokenId]) {
val prices = requireNotNull(value[tokenId.value]) {
"$tokenId is not found in the response. This shouldn't have happened."
}
return when (interval) {

View file

@ -3,6 +3,7 @@ package com.tangem.data.markets.converters
import com.tangem.datasource.api.markets.models.response.TokenMarketListResponse
import com.tangem.domain.markets.TokenMarket
import com.tangem.domain.markets.TokenQuotesShort
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.utils.converter.Converter
internal object TokenMarketListConverter : Converter<TokenMarketListResponse, List<TokenMarket>> {
@ -18,7 +19,7 @@ internal object TokenMarketListConverter : Converter<TokenMarketListResponse, Li
return value.tokens.map { token ->
TokenMarket(
id = token.id,
id = CryptoCurrency.RawID(token.id),
name = token.name,
symbol = token.symbol,
marketRating = token.marketRating,

View file

@ -2,12 +2,13 @@ package com.tangem.data.markets.converters
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.domain.markets.TokenQuotesShort
import com.tangem.domain.tokens.model.CryptoCurrency
import java.math.BigDecimal
internal object TokenQuotesShortConverter {
fun convert(tokenId: String, value: QuotesResponse): TokenQuotesShort {
val quote = requireNotNull(value.quotes[tokenId]) {
fun convert(tokenId: CryptoCurrency.RawID, value: QuotesResponse): TokenQuotesShort {
val quote = requireNotNull(value.quotes[tokenId.value]) {
"$tokenId is not found in the response. This shouldn't have happened."
}
return TokenQuotesShort(

View file

@ -37,7 +37,7 @@ internal class HotCryptoCurrencyConverter(
val currency = if (contractAddress != null && decimals != null) {
cryptoCurrencyFactory.createToken(
network = network,
rawId = value.id,
rawId = CryptoCurrency.RawID(value.id),
name = value.name,
symbol = value.symbol,
decimals = decimals,

View file

@ -579,9 +579,9 @@ internal class DefaultStakingRepository(
}
}
private fun findPrefetchedYield(yields: List<Yield>, currencyId: String, symbol: String): Yield? {
private fun findPrefetchedYield(yields: List<Yield>, currencyId: CryptoCurrency.RawID, symbol: String): Yield? {
return yields.find { yield ->
yield.tokens.any { it.coinGeckoId == currencyId && it.symbol == symbol }
yield.tokens.any { it.coinGeckoId == currencyId.value && it.symbol == symbol }
}
}

View file

@ -1,82 +0,0 @@
package com.tangem.data.tokens.paging
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.model.Token
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
/**
* A PagingSource responsible for retrieving tokens from the Tangem Tech API and adding to them quotes information.
*
* @property api Tangem Tech API
* @property quotesRepository Repository providing quotes data.
* @property dispatchers Coroutine dispatchers provider.
* @property searchText The search text used to filter tokens.
*/
internal class CoinsPagingSource(
private val api: TangemTechApi,
private val quotesRepository: QuotesRepository,
private val dispatchers: CoroutineDispatcherProvider,
private val searchText: String?,
) : PagingSource<Int, Token>() {
override fun getRefreshKey(state: PagingState<Int, Token>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(other = 1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(other = 1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Token> {
val page = params.key ?: 0
return com.tangem.utils.coroutines.runCatching(dispatchers.io) {
api.getCoins(
active = true, // TODO change when voting functionality is implemented
searchText = searchText,
offset = page * params.loadSize,
limit = params.loadSize,
).getOrThrow()
}.fold(
onSuccess = { response ->
val coinsIds = response.coins.map { coin ->
CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
body = CryptoCurrency.ID.Body.NetworkId(coin.id),
suffix = CryptoCurrency.ID.Suffix.RawID(coin.id),
)
}
try {
val quotes = quotesRepository.getQuotesSync(currenciesIds = coinsIds.toSet(), refresh = false)
LoadResult.Page(
data = CoinsResponseConverter.convert(
CoinsData(
response.coins,
response.imageHost,
quotes,
),
),
prevKey = if (page == 0) null else page.minus(other = 1),
nextKey = if (response.coins.isEmpty()) null else page.plus(other = 1),
)
} catch (t: Throwable) {
LoadResult.Error(t)
}
},
onFailure = { LoadResult.Error(it) },
)
}
}
data class CoinsData(
val coins: List<CoinsResponse.Coin>,
val imageHost: String?,
val quotes: Set<Quote>,
)

View file

@ -1,52 +0,0 @@
package com.tangem.data.tokens.paging
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.data.common.currency.getNetworkStandardType
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Token
import com.tangem.utils.converter.Converter
/**
* Converter from data model [CoinsResponse] to list of domain models [Token]
*/
internal object CoinsResponseConverter : Converter<CoinsData, List<Token>> {
override fun convert(value: CoinsData): List<Token> {
return value.coins.map { coin ->
val id = CryptoCurrency.ID(
CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
CryptoCurrency.ID.Body.NetworkId(coin.id),
CryptoCurrency.ID.Suffix.RawID(coin.id),
)
val quote = value.quotes.find { it.rawCurrencyId == id.rawCurrencyId }
Token(
id = id.rawCurrencyId ?: coin.name,
name = coin.name,
symbol = coin.symbol,
iconUrl = getIconUrl(coin.id, value.imageHost),
isAvailable = coin.active,
networks = coin.networks.mapNotNull { network ->
val blockchain = Blockchain.fromNetworkId(network.networkId) ?: return@mapNotNull null
Token.Network(
networkId = network.networkId,
standardType = getNetworkStandardType(blockchain).name,
name = blockchain.fullName,
address = network.contractAddress,
iconUrl = getIconUrl(network.networkId, value.imageHost),
decimalCount = network.decimalCount?.toInt(),
)
},
quote = quote,
)
}
}
}
internal 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

@ -486,7 +486,9 @@ internal class DefaultCurrenciesRepository(
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun getAllWalletsCryptoCurrencies(currencyRawId: String): Flow<Map<UserWallet, List<CryptoCurrency>>> {
override fun getAllWalletsCryptoCurrencies(
currencyRawId: CryptoCurrency.RawID,
): Flow<Map<UserWallet, List<CryptoCurrency>>> {
return userWalletsStore.userWallets.flatMapLatest { userWallets ->
userWallets.forEach { fetchTokensIfCacheExpired(userWallet = it, refresh = false) }
@ -496,7 +498,7 @@ internal class DefaultCurrenciesRepository(
if (userWallet.isMultiCurrency) {
getSavedUserTokensResponse(userWallet.walletId).map { storedTokens ->
val filterResponse = storedTokens.tokens.filter {
getL2CompatibilityTokenComparison(it, currencyRawId)
getL2CompatibilityTokenComparison(it, currencyRawId.value)
}
responseCurrenciesFactory.createCurrencies(

View file

@ -37,7 +37,7 @@ internal class DefaultQuotesRepository(
private val mutex = Mutex()
@OptIn(ExperimentalCoroutinesApi::class)
override fun getQuotesUpdates(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Flow<Set<Quote>> {
override fun getQuotesUpdates(currenciesIds: Set<CryptoCurrency.RawID>, refresh: Boolean): Flow<Set<Quote>> {
return appPreferencesStore.getObject<CurrenciesResponse.Currency>(
key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY,
)
@ -51,7 +51,7 @@ internal class DefaultQuotesRepository(
.flowOn(dispatchers.io)
}
override suspend fun fetchQuotes(currenciesIds: Set<CryptoCurrency.ID>) {
override suspend fun fetchQuotes(currenciesIds: Set<CryptoCurrency.RawID>) {
withContext(dispatchers.io) {
val selectedAppCurrency = requireNotNull(
value = appPreferencesStore.getObjectSyncOrNull<CurrenciesResponse.Currency>(
@ -64,7 +64,7 @@ internal class DefaultQuotesRepository(
}
}
override suspend fun getQuotesSync(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Set<Quote> {
override suspend fun getQuotesSync(currenciesIds: Set<CryptoCurrency.RawID>, refresh: Boolean): Set<Quote> {
return withContext(dispatchers.io) {
val selectedAppCurrency = requireNotNull(
value = appPreferencesStore.getObjectSyncOrNull<CurrenciesResponse.Currency>(
@ -81,7 +81,7 @@ internal class DefaultQuotesRepository(
}
}
override suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote? {
override suspend fun getQuoteSync(currencyId: CryptoCurrency.RawID): Quote? {
return withContext(dispatchers.io) {
val setOfCurrencyId = setOf(currencyId)
val quote = quotesStore.getSync(setOfCurrencyId).firstOrNull()
@ -90,7 +90,7 @@ internal class DefaultQuotesRepository(
}
private suspend fun fetchExpiredQuotes(
currenciesIds: Set<CryptoCurrency.ID>,
currenciesIds: Set<CryptoCurrency.RawID>,
appCurrencyId: String,
refresh: Boolean,
) {
@ -110,15 +110,17 @@ internal class DefaultQuotesRepository(
}
}
private suspend fun fetchQuotes(rawCurrenciesIds: Set<String>, appCurrencyId: String) {
val replacementIdsResult = quotesUnsupportedCurrenciesAdapter.replaceUnsupportedCurrencies(rawCurrenciesIds)
private suspend fun fetchQuotes(rawCurrenciesIds: Set<CryptoCurrency.RawID>, appCurrencyId: String) {
val replacementIdsResult = quotesUnsupportedCurrenciesAdapter.replaceUnsupportedCurrencies(
rawCurrenciesIds.map { it.value }.toSet(),
)
val response = safeApiCallWithTimeout(
call = {
val coinIds = replacementIdsResult.idsForRequest.joinToString(separator = ",")
tangemTechApi.getQuotes(appCurrencyId, coinIds).bind()
},
onError = { error ->
cacheRegistry.invalidate(rawCurrenciesIds.map(::getQuoteCacheKey))
cacheRegistry.invalidate(rawCurrenciesIds.map { getQuoteCacheKey(it) })
throw error
},
@ -132,21 +134,20 @@ internal class DefaultQuotesRepository(
}
private suspend fun filterExpiredCurrenciesIds(
currenciesIds: Set<CryptoCurrency.ID>,
currenciesIds: Set<CryptoCurrency.RawID>,
refresh: Boolean,
): Set<String> {
): Set<CryptoCurrency.RawID> {
return currenciesIds.fold(hashSetOf()) { acc, currencyId ->
val rawCurrencyId = currencyId.rawCurrencyId
if (rawCurrencyId != null && rawCurrencyId !in acc) {
if (currencyId !in acc) {
cacheRegistry.invokeOnExpire(
key = getQuoteCacheKey(rawCurrencyId),
key = getQuoteCacheKey(currencyId),
skipCache = refresh,
block = { acc.add(rawCurrencyId) },
block = { acc.add(currencyId) },
)
}
acc
}
}
private fun getQuoteCacheKey(rawCurrencyId: String): String = "quote_$rawCurrencyId"
private fun getQuoteCacheKey(rawCurrencyId: CryptoCurrency.RawID): String = "quote_${rawCurrencyId.value}"
}

View file

@ -6,16 +6,16 @@ import com.tangem.domain.tokens.model.Quote
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
typealias QuotesConverterValue = Pair<Set<CryptoCurrency.ID>, Set<StoredQuote>>
typealias QuotesConverterValue = Pair<Set<CryptoCurrency.RawID>, Set<StoredQuote>>
internal class QuotesConverter : Converter<QuotesConverterValue, Set<Quote>> {
override fun convert(value: QuotesConverterValue): Set<Quote> {
val (setOfCurrencyId, setOfStoredQuote) = value
return setOfCurrencyId.mapTo(hashSetOf()) { id ->
setOfStoredQuote.find { id.rawCurrencyId == it.rawCurrencyId }
setOfStoredQuote.find { id.value == it.rawCurrencyId }
?.let(::convertExistStoredQuote)
?: Quote.Empty(id.rawCurrencyId)
?: Quote.Empty(id)
}
}
@ -23,7 +23,7 @@ internal class QuotesConverter : Converter<QuotesConverterValue, Set<Quote>> {
val (rawCurrencyId, responseQuote) = storedQuote
return Quote.Value(
rawCurrencyId = rawCurrencyId,
rawCurrencyId = CryptoCurrency.RawID(rawCurrencyId),
fiatRate = responseQuote.price ?: BigDecimal.ZERO,
priceChange = (responseQuote.priceChange24h ?: BigDecimal.ZERO).movePointLeft(2),
)