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

@ -46,7 +46,7 @@ internal class DefaultHotCryptoLoader @Inject constructor(
override suspend fun update(currencies: List<CryptoCurrency>) { override suspend fun update(currencies: List<CryptoCurrency>) {
updateInternal { hotToken -> updateInternal { hotToken ->
currencies.none { currencies.none {
it.id.rawCurrencyId == hotToken.id && it.id.rawCurrencyId?.value == hotToken.id &&
(it as? CryptoCurrency.Token)?.contractAddress == hotToken.contractAddress && (it as? CryptoCurrency.Token)?.contractAddress == hotToken.contractAddress &&
it.network.id.value == hotToken.networkId it.network.id.value == hotToken.networkId
} }

View file

@ -11,11 +11,9 @@ internal class DefaultQuotesStore(
private val dataStore: StringKeyDataStore<StoredQuote>, private val dataStore: StringKeyDataStore<StoredQuote>,
) : QuotesStore { ) : QuotesStore {
override fun get(currenciesIds: Set<CryptoCurrency.ID>): Flow<Set<StoredQuote>> { override fun get(currenciesIds: Set<CryptoCurrency.RawID>): Flow<Set<StoredQuote>> {
return channelFlow { return channelFlow {
val flows = currenciesIds.mapNotNull { currencyId -> val flows = currenciesIds.map { currencyId -> dataStore.get(currencyId.value) }
currencyId.rawCurrencyId?.let(dataStore::get)
}
if (dataStore.isEmpty() || flows.isEmpty()) { if (dataStore.isEmpty() || flows.isEmpty()) {
send(emptySet()) send(emptySet())
@ -30,12 +28,8 @@ internal class DefaultQuotesStore(
} }
} }
override suspend fun getSync(currenciesIds: Set<CryptoCurrency.ID>): Set<StoredQuote> { override suspend fun getSync(currenciesIds: Set<CryptoCurrency.RawID>): Set<StoredQuote> {
return currenciesIds.mapNotNull { currencyId -> return currenciesIds.mapNotNull { currencyId -> dataStore.getSyncOrNull(currencyId.value) }.toSet()
currencyId.rawCurrencyId?.let {
dataStore.getSyncOrNull(it)
}
}.toSet()
} }
override suspend fun store(response: QuotesResponse) { override suspend fun store(response: QuotesResponse) {

View file

@ -7,9 +7,9 @@ import kotlinx.coroutines.flow.Flow
interface QuotesStore { interface QuotesStore {
fun get(currenciesIds: Set<CryptoCurrency.ID>): Flow<Set<StoredQuote>> fun get(currenciesIds: Set<CryptoCurrency.RawID>): Flow<Set<StoredQuote>>
suspend fun getSync(currenciesIds: Set<CryptoCurrency.ID>): Set<StoredQuote> suspend fun getSync(currenciesIds: Set<CryptoCurrency.RawID>): Set<StoredQuote>
suspend fun store(response: QuotesResponse) suspend fun store(response: QuotesResponse)
} }

View file

@ -18,7 +18,7 @@ class CryptoCurrencyFactory(
@Suppress("LongParameterList") // Yep, it's long @Suppress("LongParameterList") // Yep, it's long
fun createToken( fun createToken(
network: Network, network: Network,
rawId: String?, rawId: CryptoCurrency.RawID?,
name: String, name: String,
symbol: String, symbol: String,
decimals: Int, decimals: Int,
@ -125,7 +125,7 @@ class CryptoCurrencyFactory(
symbol = cryptoCurrency.symbol, symbol = cryptoCurrency.symbol,
contractAddress = cryptoCurrency.contractAddress, contractAddress = cryptoCurrency.contractAddress,
decimals = cryptoCurrency.decimals, decimals = cryptoCurrency.decimals,
id = cryptoCurrency.id.rawCurrencyId, id = cryptoCurrency.id.rawCurrencyId?.value,
) )
val blockchain = Blockchain.fromNetworkId(cryptoCurrency.network.backendId) ?: Blockchain.Unknown val blockchain = Blockchain.fromNetworkId(cryptoCurrency.network.backendId) ?: Blockchain.Unknown
val id = getTokenId(network, sdkToken) 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.blockchain.common.IconsUtil
import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse 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.CryptoCurrency.ID
import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.Network
import com.tangem.blockchain.common.Token as SdkToken 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 { fun getTokenId(network: Network, sdkToken: SdkToken): ID {
val sdkTokenId = 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) { val suffix = if (rawTokenId == null) {
CustomCurrencyIdSuffix(contractAddress) CustomCurrencyIdSuffix(contractAddress)
} else { } else {
CurrencyIdSuffix(rawTokenId, contractAddress) CurrencyIdSuffix(rawTokenId.value, contractAddress)
} }
return ID(TOKEN_ID_PREFIX, getCurrencyIdBody(network), suffix) 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? { fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? {
val tokenId = token.id val tokenId = token.id
val rawID = tokenId?.let { CryptoCurrency.RawID(it) }
return if (tokenId == null) { return if (rawID == null) {
IconsUtil.getTokenIconUri(blockchain, token)?.toString() IconsUtil.getTokenIconUri(blockchain, token)?.toString()
} else { } else {
getTokenIconUrlFromDefaultHost(tokenId) getTokenIconUrlFromDefaultHost(rawID)
} }
} }
@ -59,7 +62,7 @@ fun getCoinIconUrl(blockchain: Blockchain): String? {
val coinId = when (blockchain) { val coinId = when (blockchain) {
Blockchain.Unknown -> null Blockchain.Unknown -> null
else -> blockchain.toCoinId() else -> blockchain.toCoinId()
} }?.let { CryptoCurrency.RawID(it) }
return coinId?.let(::getTokenIconUrlFromDefaultHost) 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 { return buildString {
append(DEFAULT_TOKENS_ICONS_HOST) append(DEFAULT_TOKENS_ICONS_HOST)
append('/') append('/')
append(TOKEN_ICON_SIZE) append(TOKEN_ICON_SIZE)
append('/') append('/')
append(tokenId) append(tokenId.value)
append('.') append('.')
append(TOKEN_ICON_EXT) append(TOKEN_ICON_EXT)
} }

View file

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

View file

@ -122,7 +122,7 @@ internal class DefaultCustomTokensRepository(
if (coinNetwork != null) { if (coinNetwork != null) {
cryptoCurrencyFactory.createToken( cryptoCurrencyFactory.createToken(
network = network, network = network,
rawId = coin.id, rawId = CryptoCurrency.RawID(coin.id),
name = coin.name, name = coin.name,
symbol = coin.symbol, symbol = coin.symbol,
decimals = coinNetwork.decimalCount!!.toInt(), decimals = coinNetwork.decimalCount!!.toInt(),
@ -154,7 +154,7 @@ internal class DefaultCustomTokensRepository(
override suspend fun createToken( override suspend fun createToken(
managedCryptoCurrency: ManagedCryptoCurrency.Token, managedCryptoCurrency: ManagedCryptoCurrency.Token,
sourceNetwork: ManagedCryptoCurrency.SourceNetwork.Default, sourceNetwork: ManagedCryptoCurrency.SourceNetwork.Default,
rawId: String?, rawId: CryptoCurrency.RawID?,
): CryptoCurrency.Token { ): CryptoCurrency.Token {
return cryptoCurrencyFactory.createToken( return cryptoCurrencyFactory.createToken(
network = sourceNetwork.network, 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
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.Network
internal class ManagedCryptoCurrencyFactory( internal class ManagedCryptoCurrencyFactory(
@ -114,7 +115,11 @@ internal class ManagedCryptoCurrencyFactory(
) )
} else { } else {
ManagedCryptoCurrency.Custom.Token( 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, name = token.name,
symbol = token.symbol, symbol = token.symbol,
iconUrl = token.id?.let { getIconUrl(it) }, iconUrl = token.id?.let { getIconUrl(it) },

View file

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

View file

@ -125,10 +125,10 @@ internal class DefaultMarketsTokenRepository(
override suspend fun getChart( override suspend fun getChart(
fiatCurrencyCode: String, fiatCurrencyCode: String,
interval: PriceChangeInterval, interval: PriceChangeInterval,
tokenId: String, tokenId: CryptoCurrency.RawID,
tokenSymbol: String, tokenSymbol: String,
) = withContext(dispatcherProvider.io) { ) = withContext(dispatcherProvider.io) {
val mappedTokenId = getTokenIdIfL2Network(tokenId) val mappedTokenId = getTokenIdIfL2Network(tokenId.value)
val response = marketsApi.getCoinChart( val response = marketsApi.getCoinChart(
currency = fiatCurrencyCode, currency = fiatCurrencyCode,
coinId = mappedTokenId, coinId = mappedTokenId,
@ -161,10 +161,10 @@ internal class DefaultMarketsTokenRepository(
override suspend fun getChartPreview( override suspend fun getChartPreview(
fiatCurrencyCode: String, fiatCurrencyCode: String,
interval: PriceChangeInterval, interval: PriceChangeInterval,
tokenId: String, tokenId: CryptoCurrency.RawID,
tokenSymbol: String, tokenSymbol: String,
) = withContext(dispatcherProvider.io) { ) = withContext(dispatcherProvider.io) {
val mappedTokenId = getTokenIdIfL2Network(tokenId) val mappedTokenId = getTokenIdIfL2Network(tokenId.value)
val chart = catchListErrorAndSendEvent { val chart = catchListErrorAndSendEvent {
marketsApi.getCoinsListCharts( marketsApi.getCoinsListCharts(
@ -193,7 +193,7 @@ internal class DefaultMarketsTokenRepository(
override suspend fun getTokenInfo( override suspend fun getTokenInfo(
fiatCurrencyCode: String, fiatCurrencyCode: String,
tokenId: String, tokenId: CryptoCurrency.RawID,
tokenSymbol: String, tokenSymbol: String,
languageCode: String, languageCode: String,
) = withContext(dispatcherProvider.io) { ) = withContext(dispatcherProvider.io) {
@ -203,16 +203,16 @@ internal class DefaultMarketsTokenRepository(
) { ) {
marketsApi.getCoinMarketData( marketsApi.getCoinMarketData(
currency = fiatCurrencyCode, currency = fiatCurrencyCode,
coinId = tokenId, coinId = tokenId.value,
language = languageCode, language = languageCode,
).getOrThrow() ).getOrThrow()
} }
val resultResponse = result.applyL2Compatibility(tokenId) val resultResponse = result.applyL2Compatibility(tokenId.value)
return@withContext tokenMarketInfoConverter.convert(resultResponse) 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) { withContext(dispatcherProvider.io) {
// for second markets iteration we should use extended api method with all required fields // for second markets iteration we should use extended api method with all required fields
@ -222,7 +222,7 @@ internal class DefaultMarketsTokenRepository(
) { ) {
tangemTechApi.getQuotes( tangemTechApi.getQuotes(
currencyId = fiatCurrencyCode, currencyId = fiatCurrencyCode,
coinIds = tokenId, coinIds = tokenId.value,
fields = marketsQuoteFields.joinToString(separator = ","), fields = marketsQuoteFields.joinToString(separator = ","),
).getOrThrow() ).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) { return withContext(dispatcherProvider.io) {
cacheRegistry.invokeOnExpire(key = "coins/$tokenId/exchanges", skipCache = false) { 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) 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.PriceChangeInterval
import com.tangem.domain.markets.TokenMarket import com.tangem.domain.markets.TokenMarket
import com.tangem.domain.markets.TokenMarketListConfig import com.tangem.domain.markets.TokenMarketListConfig
import com.tangem.domain.tokens.model.CryptoCurrency
internal object TokenMarketChartsConverter { internal object TokenMarketChartsConverter {
fun convert( fun convert(
chartsToCopy: TokenMarket.Charts, chartsToCopy: TokenMarket.Charts,
tokenId: String, tokenId: CryptoCurrency.RawID,
interval: TokenMarketListConfig.Interval, interval: TokenMarketListConfig.Interval,
value: TokenMarketChartListResponse, value: TokenMarketChartListResponse,
): TokenMarket.Charts { ): 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." "$tokenId is not found in the response. This shouldn't have happened."
} }
return when (interval) { 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.datasource.api.markets.models.response.TokenMarketListResponse
import com.tangem.domain.markets.TokenMarket import com.tangem.domain.markets.TokenMarket
import com.tangem.domain.markets.TokenQuotesShort import com.tangem.domain.markets.TokenQuotesShort
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.utils.converter.Converter import com.tangem.utils.converter.Converter
internal object TokenMarketListConverter : Converter<TokenMarketListResponse, List<TokenMarket>> { internal object TokenMarketListConverter : Converter<TokenMarketListResponse, List<TokenMarket>> {
@ -18,7 +19,7 @@ internal object TokenMarketListConverter : Converter<TokenMarketListResponse, Li
return value.tokens.map { token -> return value.tokens.map { token ->
TokenMarket( TokenMarket(
id = token.id, id = CryptoCurrency.RawID(token.id),
name = token.name, name = token.name,
symbol = token.symbol, symbol = token.symbol,
marketRating = token.marketRating, 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.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.domain.markets.TokenQuotesShort import com.tangem.domain.markets.TokenQuotesShort
import com.tangem.domain.tokens.model.CryptoCurrency
import java.math.BigDecimal import java.math.BigDecimal
internal object TokenQuotesShortConverter { internal object TokenQuotesShortConverter {
fun convert(tokenId: String, value: QuotesResponse): TokenQuotesShort { fun convert(tokenId: CryptoCurrency.RawID, value: QuotesResponse): TokenQuotesShort {
val quote = requireNotNull(value.quotes[tokenId]) { val quote = requireNotNull(value.quotes[tokenId.value]) {
"$tokenId is not found in the response. This shouldn't have happened." "$tokenId is not found in the response. This shouldn't have happened."
} }
return TokenQuotesShort( return TokenQuotesShort(

View file

@ -37,7 +37,7 @@ internal class HotCryptoCurrencyConverter(
val currency = if (contractAddress != null && decimals != null) { val currency = if (contractAddress != null && decimals != null) {
cryptoCurrencyFactory.createToken( cryptoCurrencyFactory.createToken(
network = network, network = network,
rawId = value.id, rawId = CryptoCurrency.RawID(value.id),
name = value.name, name = value.name,
symbol = value.symbol, symbol = value.symbol,
decimals = decimals, 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 -> 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) @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 -> return userWalletsStore.userWallets.flatMapLatest { userWallets ->
userWallets.forEach { fetchTokensIfCacheExpired(userWallet = it, refresh = false) } userWallets.forEach { fetchTokensIfCacheExpired(userWallet = it, refresh = false) }
@ -496,7 +498,7 @@ internal class DefaultCurrenciesRepository(
if (userWallet.isMultiCurrency) { if (userWallet.isMultiCurrency) {
getSavedUserTokensResponse(userWallet.walletId).map { storedTokens -> getSavedUserTokensResponse(userWallet.walletId).map { storedTokens ->
val filterResponse = storedTokens.tokens.filter { val filterResponse = storedTokens.tokens.filter {
getL2CompatibilityTokenComparison(it, currencyRawId) getL2CompatibilityTokenComparison(it, currencyRawId.value)
} }
responseCurrenciesFactory.createCurrencies( responseCurrenciesFactory.createCurrencies(

View file

@ -37,7 +37,7 @@ internal class DefaultQuotesRepository(
private val mutex = Mutex() private val mutex = Mutex()
@OptIn(ExperimentalCoroutinesApi::class) @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>( return appPreferencesStore.getObject<CurrenciesResponse.Currency>(
key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY, key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY,
) )
@ -51,7 +51,7 @@ internal class DefaultQuotesRepository(
.flowOn(dispatchers.io) .flowOn(dispatchers.io)
} }
override suspend fun fetchQuotes(currenciesIds: Set<CryptoCurrency.ID>) { override suspend fun fetchQuotes(currenciesIds: Set<CryptoCurrency.RawID>) {
withContext(dispatchers.io) { withContext(dispatchers.io) {
val selectedAppCurrency = requireNotNull( val selectedAppCurrency = requireNotNull(
value = appPreferencesStore.getObjectSyncOrNull<CurrenciesResponse.Currency>( 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) { return withContext(dispatchers.io) {
val selectedAppCurrency = requireNotNull( val selectedAppCurrency = requireNotNull(
value = appPreferencesStore.getObjectSyncOrNull<CurrenciesResponse.Currency>( 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) { return withContext(dispatchers.io) {
val setOfCurrencyId = setOf(currencyId) val setOfCurrencyId = setOf(currencyId)
val quote = quotesStore.getSync(setOfCurrencyId).firstOrNull() val quote = quotesStore.getSync(setOfCurrencyId).firstOrNull()
@ -90,7 +90,7 @@ internal class DefaultQuotesRepository(
} }
private suspend fun fetchExpiredQuotes( private suspend fun fetchExpiredQuotes(
currenciesIds: Set<CryptoCurrency.ID>, currenciesIds: Set<CryptoCurrency.RawID>,
appCurrencyId: String, appCurrencyId: String,
refresh: Boolean, refresh: Boolean,
) { ) {
@ -110,15 +110,17 @@ internal class DefaultQuotesRepository(
} }
} }
private suspend fun fetchQuotes(rawCurrenciesIds: Set<String>, appCurrencyId: String) { private suspend fun fetchQuotes(rawCurrenciesIds: Set<CryptoCurrency.RawID>, appCurrencyId: String) {
val replacementIdsResult = quotesUnsupportedCurrenciesAdapter.replaceUnsupportedCurrencies(rawCurrenciesIds) val replacementIdsResult = quotesUnsupportedCurrenciesAdapter.replaceUnsupportedCurrencies(
rawCurrenciesIds.map { it.value }.toSet(),
)
val response = safeApiCallWithTimeout( val response = safeApiCallWithTimeout(
call = { call = {
val coinIds = replacementIdsResult.idsForRequest.joinToString(separator = ",") val coinIds = replacementIdsResult.idsForRequest.joinToString(separator = ",")
tangemTechApi.getQuotes(appCurrencyId, coinIds).bind() tangemTechApi.getQuotes(appCurrencyId, coinIds).bind()
}, },
onError = { error -> onError = { error ->
cacheRegistry.invalidate(rawCurrenciesIds.map(::getQuoteCacheKey)) cacheRegistry.invalidate(rawCurrenciesIds.map { getQuoteCacheKey(it) })
throw error throw error
}, },
@ -132,21 +134,20 @@ internal class DefaultQuotesRepository(
} }
private suspend fun filterExpiredCurrenciesIds( private suspend fun filterExpiredCurrenciesIds(
currenciesIds: Set<CryptoCurrency.ID>, currenciesIds: Set<CryptoCurrency.RawID>,
refresh: Boolean, refresh: Boolean,
): Set<String> { ): Set<CryptoCurrency.RawID> {
return currenciesIds.fold(hashSetOf()) { acc, currencyId -> return currenciesIds.fold(hashSetOf()) { acc, currencyId ->
val rawCurrencyId = currencyId.rawCurrencyId if (currencyId !in acc) {
if (rawCurrencyId != null && rawCurrencyId !in acc) {
cacheRegistry.invokeOnExpire( cacheRegistry.invokeOnExpire(
key = getQuoteCacheKey(rawCurrencyId), key = getQuoteCacheKey(currencyId),
skipCache = refresh, skipCache = refresh,
block = { acc.add(rawCurrencyId) }, block = { acc.add(currencyId) },
) )
} }
acc 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 com.tangem.utils.converter.Converter
import java.math.BigDecimal 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>> { internal class QuotesConverter : Converter<QuotesConverterValue, Set<Quote>> {
override fun convert(value: QuotesConverterValue): Set<Quote> { override fun convert(value: QuotesConverterValue): Set<Quote> {
val (setOfCurrencyId, setOfStoredQuote) = value val (setOfCurrencyId, setOfStoredQuote) = value
return setOfCurrencyId.mapTo(hashSetOf()) { id -> return setOfCurrencyId.mapTo(hashSetOf()) { id ->
setOfStoredQuote.find { id.rawCurrencyId == it.rawCurrencyId } setOfStoredQuote.find { id.value == it.rawCurrencyId }
?.let(::convertExistStoredQuote) ?.let(::convertExistStoredQuote)
?: Quote.Empty(id.rawCurrencyId) ?: Quote.Empty(id)
} }
} }
@ -23,7 +23,7 @@ internal class QuotesConverter : Converter<QuotesConverterValue, Set<Quote>> {
val (rawCurrencyId, responseQuote) = storedQuote val (rawCurrencyId, responseQuote) = storedQuote
return Quote.Value( return Quote.Value(
rawCurrencyId = rawCurrencyId, rawCurrencyId = CryptoCurrency.RawID(rawCurrencyId),
fiatRate = responseQuote.price ?: BigDecimal.ZERO, fiatRate = responseQuote.price ?: BigDecimal.ZERO,
priceChange = (responseQuote.priceChange24h ?: BigDecimal.ZERO).movePointLeft(2), priceChange = (responseQuote.priceChange24h ?: BigDecimal.ZERO).movePointLeft(2),
) )

View file

@ -217,7 +217,7 @@ class DefaultWalletManagersFacade(
symbol = currency.symbol, symbol = currency.symbol,
contractAddress = currency.contractAddress, contractAddress = currency.contractAddress,
decimals = currency.decimals, decimals = currency.decimals,
id = currency.id.rawCurrencyId, id = currency.id.rawCurrencyId?.value,
) )
TransactionHistoryRequest.FilterType.Contract(blockchainToken) TransactionHistoryRequest.FilterType.Contract(blockchainToken)
} }
@ -255,7 +255,7 @@ class DefaultWalletManagersFacade(
symbol = currency.symbol, symbol = currency.symbol,
contractAddress = currency.contractAddress, contractAddress = currency.contractAddress,
decimals = currency.decimals, decimals = currency.decimals,
id = currency.id.rawCurrencyId, id = currency.id.rawCurrencyId?.value,
) )
TransactionHistoryRequest.FilterType.Contract(blockchainToken) TransactionHistoryRequest.FilterType.Contract(blockchainToken)
} }

View file

@ -1,5 +1,6 @@
package com.tangem.domain.walletmanager.model package com.tangem.domain.walletmanager.model
import com.tangem.domain.tokens.model.CryptoCurrency
import java.math.BigDecimal import java.math.BigDecimal
sealed class CryptoCurrencyAmount { sealed class CryptoCurrencyAmount {
@ -9,7 +10,7 @@ sealed class CryptoCurrencyAmount {
data class Coin(override val value: BigDecimal) : CryptoCurrencyAmount() data class Coin(override val value: BigDecimal) : CryptoCurrencyAmount()
data class Token( data class Token(
val tokenId: String?, val tokenId: CryptoCurrency.RawID?,
val tokenContractAddress: String, val tokenContractAddress: String,
override val value: BigDecimal, override val value: BigDecimal,
) : CryptoCurrencyAmount() ) : CryptoCurrencyAmount()

View file

@ -15,7 +15,7 @@ internal class CryptoCurrencyTypeConverter : Converter<CryptoCurrency, CryptoCur
symbol = value.symbol, symbol = value.symbol,
contractAddress = value.contractAddress, contractAddress = value.contractAddress,
decimals = value.decimals, decimals = value.decimals,
id = value.id.rawCurrencyId, id = value.id.rawCurrencyId?.value,
), ),
) )
} }

View file

@ -8,7 +8,7 @@ internal class SdkTokenConverter : Converter<CryptoCurrency.Token, SdkToken> {
override fun convert(value: CryptoCurrency.Token): SdkToken { override fun convert(value: CryptoCurrency.Token): SdkToken {
return SdkToken( return SdkToken(
id = value.id.rawCurrencyId, id = value.id.rawCurrencyId?.value,
name = value.name, name = value.name,
symbol = value.symbol, symbol = value.symbol,
contractAddress = value.contractAddress, contractAddress = value.contractAddress,

View file

@ -2,6 +2,7 @@ package com.tangem.domain.walletmanager.utils
import com.tangem.blockchain.common.* import com.tangem.blockchain.common.*
import com.tangem.blockchainsdk.utils.amountToCreateAccount import com.tangem.blockchainsdk.utils.amountToCreateAccount
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.walletmanager.model.Address import com.tangem.domain.walletmanager.model.Address
import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount
import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction
@ -84,7 +85,11 @@ internal class UpdateWalletManagerResultFactory {
val demoAmounts = hashSetOf<CryptoCurrencyAmount>(CryptoCurrencyAmount.Coin(amountValue)) val demoAmounts = hashSetOf<CryptoCurrencyAmount>(CryptoCurrencyAmount.Coin(amountValue))
return tokens.mapTo(demoAmounts) { token -> return tokens.mapTo(demoAmounts) { token ->
CryptoCurrencyAmount.Token(token.id, token.contractAddress, amountValue) CryptoCurrencyAmount.Token(
tokenId = token.id?.let { CryptoCurrency.RawID(it) },
tokenContractAddress = token.contractAddress,
value = amountValue,
)
} }
} }
@ -107,7 +112,7 @@ internal class UpdateWalletManagerResultFactory {
private fun createCurrencyAmount(amount: Amount): CryptoCurrencyAmount? { private fun createCurrencyAmount(amount: Amount): CryptoCurrencyAmount? {
return when (val type = amount.type) { return when (val type = amount.type) {
is AmountType.Token -> CryptoCurrencyAmount.Token( is AmountType.Token -> CryptoCurrencyAmount.Token(
tokenId = type.token.id, tokenId = type.token.id?.let { CryptoCurrency.RawID(it) },
tokenContractAddress = type.token.contractAddress, tokenContractAddress = type.token.contractAddress,
value = getCurrencyAmountValue(amount) ?: return null, value = getCurrencyAmountValue(amount) ?: return null,
) )

View file

@ -111,7 +111,7 @@ class SaveManagedTokensUseCase(
is ManagedCryptoCurrency.SourceNetwork.Default -> customTokensRepository.createToken( is ManagedCryptoCurrency.SourceNetwork.Default -> customTokensRepository.createToken(
managedCryptoCurrency = token, managedCryptoCurrency = token,
sourceNetwork = sourceNetwork, sourceNetwork = sourceNetwork,
rawId = token.id.value, rawId = CryptoCurrency.RawID(token.id.value),
) )
is ManagedCryptoCurrency.SourceNetwork.Main -> customTokensRepository.createCoin( is ManagedCryptoCurrency.SourceNetwork.Main -> customTokensRepository.createCoin(
userWalletId = userWalletId, userWalletId = userWalletId,

View file

@ -33,7 +33,7 @@ interface CustomTokensRepository {
suspend fun createToken( suspend fun createToken(
managedCryptoCurrency: ManagedCryptoCurrency.Token, managedCryptoCurrency: ManagedCryptoCurrency.Token,
sourceNetwork: ManagedCryptoCurrency.SourceNetwork.Default, sourceNetwork: ManagedCryptoCurrency.SourceNetwork.Default,
rawId: String?, rawId: CryptoCurrency.RawID?,
): CryptoCurrency.Token ): CryptoCurrency.Token
suspend fun createCustomToken( suspend fun createCustomToken(

View file

@ -5,6 +5,7 @@ plugins {
} }
dependencies { dependencies {
api(projects.domain.tokens.models)
implementation(projects.domain.core) implementation(projects.domain.core)
implementation(deps.kotlin.serialization) implementation(deps.kotlin.serialization)

View file

@ -1,9 +1,10 @@
package com.tangem.domain.markets package com.tangem.domain.markets
import com.tangem.domain.tokens.model.CryptoCurrency
import java.math.BigDecimal import java.math.BigDecimal
data class TokenMarket( data class TokenMarket(
val id: String, val id: CryptoCurrency.RawID,
val name: String, val name: String,
val symbol: String, val symbol: String,
val marketRating: Int?, val marketRating: Int?,

View file

@ -1,11 +1,12 @@
package com.tangem.domain.markets package com.tangem.domain.markets
import com.tangem.domain.core.serialization.SerializedBigDecimal import com.tangem.domain.core.serialization.SerializedBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrency
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class TokenMarketParams( data class TokenMarketParams(
val id: String, val id: CryptoCurrency.RawID,
val name: String, val name: String,
val symbol: String, val symbol: String,
val tokenQuotes: Quotes, val tokenQuotes: Quotes,

View file

@ -16,8 +16,10 @@ class GetCurrencyQuotesUseCase(
interval: PriceChangeInterval, interval: PriceChangeInterval,
refresh: Boolean, refresh: Boolean,
): Flow<Option<Quote.Value>> { ): Flow<Option<Quote.Value>> {
val rawId = currencyID.rawCurrencyId ?: return flowOf(None)
return quotesRepository.getQuotesUpdates( return quotesRepository.getQuotesUpdates(
currenciesIds = setOf(currencyID), currenciesIds = setOf(rawId),
refresh = refresh, refresh = refresh,
).map { it.filterIsInstance<Quote.Value>().firstOrNull().toOption() }.catch { emit(None) } ).map { it.filterIsInstance<Quote.Value>().firstOrNull().toOption() }.catch { emit(None) }
} }

View file

@ -2,6 +2,7 @@ package com.tangem.domain.markets
import arrow.core.Either import arrow.core.Either
import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.domain.tokens.model.CryptoCurrency
/** /**
* Get token exchanges use case * Get token exchanges use case
@ -12,7 +13,7 @@ class GetTokenExchangesUseCase(
private val marketsTokenRepository: MarketsTokenRepository, private val marketsTokenRepository: MarketsTokenRepository,
) { ) {
suspend operator fun invoke(tokenId: String): Either<Throwable, List<TokenMarketExchange>> { suspend operator fun invoke(tokenId: CryptoCurrency.RawID): Either<Throwable, List<TokenMarketExchange>> {
return Either.catch { marketsTokenRepository.getTokenExchanges(tokenId = tokenId) } return Either.catch { marketsTokenRepository.getTokenExchanges(tokenId = tokenId) }
} }
} }

View file

@ -3,13 +3,14 @@ package com.tangem.domain.markets
import arrow.core.Either import arrow.core.Either
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.domain.tokens.model.CryptoCurrency
class GetTokenFullQuotesUseCase( class GetTokenFullQuotesUseCase(
private val marketsTokenRepository: MarketsTokenRepository, private val marketsTokenRepository: MarketsTokenRepository,
) { ) {
suspend operator fun invoke( suspend operator fun invoke(
appCurrency: AppCurrency, appCurrency: AppCurrency,
tokenId: String, tokenId: CryptoCurrency.RawID,
tokenSymbol: String, tokenSymbol: String,
): Either<Unit, TokenQuotes> { ): Either<Unit, TokenQuotes> {
return Either.catch { return Either.catch {

View file

@ -3,6 +3,7 @@ package com.tangem.domain.markets
import arrow.core.Either import arrow.core.Either
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.utils.SupportedLanguages import com.tangem.utils.SupportedLanguages
class GetTokenMarketInfoUseCase( class GetTokenMarketInfoUseCase(
@ -11,7 +12,7 @@ class GetTokenMarketInfoUseCase(
suspend operator fun invoke( suspend operator fun invoke(
appCurrency: AppCurrency, appCurrency: AppCurrency,
tokenId: String, tokenId: CryptoCurrency.RawID,
tokenSymbol: String, tokenSymbol: String,
): Either<Unit, TokenMarketInfo> { ): Either<Unit, TokenMarketInfo> {
return Either.catch { return Either.catch {

View file

@ -3,6 +3,7 @@ package com.tangem.domain.markets
import arrow.core.Either import arrow.core.Either
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.domain.tokens.model.CryptoCurrency
class GetTokenPriceChartUseCase( class GetTokenPriceChartUseCase(
private val marketsTokenRepository: MarketsTokenRepository, private val marketsTokenRepository: MarketsTokenRepository,
@ -11,7 +12,7 @@ class GetTokenPriceChartUseCase(
suspend operator fun invoke( suspend operator fun invoke(
appCurrency: AppCurrency, appCurrency: AppCurrency,
interval: PriceChangeInterval, interval: PriceChangeInterval,
tokenId: String, tokenId: CryptoCurrency.RawID,
tokenSymbol: String, tokenSymbol: String,
preview: Boolean, preview: Boolean,
): Either<Unit, TokenChart> { ): Either<Unit, TokenChart> {

View file

@ -15,25 +15,29 @@ interface MarketsTokenRepository {
suspend fun getChart( suspend fun getChart(
fiatCurrencyCode: String, fiatCurrencyCode: String,
interval: PriceChangeInterval, interval: PriceChangeInterval,
tokenId: String, tokenId: CryptoCurrency.RawID,
tokenSymbol: String, tokenSymbol: String,
): TokenChart ): TokenChart
suspend fun getChartPreview( suspend fun getChartPreview(
fiatCurrencyCode: String, fiatCurrencyCode: String,
interval: PriceChangeInterval, interval: PriceChangeInterval,
tokenId: String, tokenId: CryptoCurrency.RawID,
tokenSymbol: String, tokenSymbol: String,
): TokenChart ): TokenChart
suspend fun getTokenInfo( suspend fun getTokenInfo(
fiatCurrencyCode: String, fiatCurrencyCode: String,
tokenId: String, tokenId: CryptoCurrency.RawID,
tokenSymbol: String, tokenSymbol: String,
languageCode: String, languageCode: String,
): TokenMarketInfo ): TokenMarketInfo
suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String, tokenSymbol: String): TokenQuotes suspend fun getTokenQuotes(
fiatCurrencyCode: String,
tokenId: CryptoCurrency.RawID,
tokenSymbol: String,
): TokenQuotes
suspend fun createCryptoCurrency( suspend fun createCryptoCurrency(
userWalletId: UserWalletId, userWalletId: UserWalletId,
@ -46,5 +50,5 @@ interface MarketsTokenRepository {
* *
* @param tokenId token id * @param tokenId token id
*/ */
suspend fun getTokenExchanges(tokenId: String): List<TokenMarketExchange> suspend fun getTokenExchanges(tokenId: CryptoCurrency.RawID): List<TokenMarketExchange>
} }

View file

@ -21,8 +21,6 @@ data class Yield(
val preferredValidators: List<Validator> val preferredValidators: List<Validator>
get() = validators.filter { it.preferred } get() = validators.filter { it.preferred }
fun getCurrentToken(rawCurrencyId: String?) = tokens.firstOrNull { rawCurrencyId == it.coinGeckoId } ?: token
@Serializable @Serializable
data class Status( data class Status(
val enter: Boolean, val enter: Boolean,

View file

@ -93,7 +93,7 @@ sealed class CryptoCurrency {
} }
/** Represents a raw cryptocurrency ID. If it is a custom token, the value will be `null`. */ /** Represents a raw cryptocurrency ID. If it is a custom token, the value will be `null`. */
val rawCurrencyId: String? get() = (suffix as? Suffix.RawID)?.rawId val rawCurrencyId: RawID? get() = (suffix as? Suffix.RawID)?.rawId?.let { RawID(it) }
val contractAddress: String? get() = (suffix as? Suffix.RawID)?.contractAddress val contractAddress: String? get() = (suffix as? Suffix.RawID)?.contractAddress
@ -196,6 +196,16 @@ sealed class CryptoCurrency {
} }
} }
/**
* Represents a raw cryptocurrency ID. Used for backend calls.
* Use with caution, as it does not provide the same level of uniqueness as [ID].
*/
@Serializable
@JvmInline
value class RawID(val value: String) {
override fun toString(): String = value
}
protected fun checkProperties() { protected fun checkProperties() {
require(name.isNotBlank()) { "Crypto currency name must not be blank" } require(name.isNotBlank()) { "Crypto currency name must not be blank" }
require(symbol.isNotBlank()) { "Crypto currency symbol must not be blank" } require(symbol.isNotBlank()) { "Crypto currency symbol must not be blank" }

View file

@ -4,14 +4,14 @@ import java.math.BigDecimal
sealed interface Quote { sealed interface Quote {
val rawCurrencyId: String? val rawCurrencyId: CryptoCurrency.RawID
/** /**
* Represents unknown financial information for a specific cryptocurrency. * Represents unknown financial information for a specific cryptocurrency.
* *
* @property rawCurrencyId The raw cryptocurrency ID. If it is a custom token, the value will be `null`. * @property rawCurrencyId The raw cryptocurrency ID.
*/ */
data class Empty(override val rawCurrencyId: String?) : Quote data class Empty(override val rawCurrencyId: CryptoCurrency.RawID) : Quote
/** /**
* Represents financial information for a specific cryptocurrency, including its fiat exchange rate and price change. * Represents financial information for a specific cryptocurrency, including its fiat exchange rate and price change.
@ -21,7 +21,7 @@ sealed interface Quote {
* @property priceChange The price change for the cryptocurrency. * @property priceChange The price change for the cryptocurrency.
*/ */
data class Value( data class Value(
override val rawCurrencyId: String, override val rawCurrencyId: CryptoCurrency.RawID,
val fiatRate: BigDecimal, val fiatRate: BigDecimal,
val priceChange: BigDecimal, val priceChange: BigDecimal,
) : Quote ) : Quote

View file

@ -0,0 +1,7 @@
package com.tangem.domain.tokens.model.staking
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.model.CryptoCurrency
fun Yield.getCurrentToken(rawCurrencyId: CryptoCurrency.RawID?) =
tokens.firstOrNull { rawCurrencyId?.value == it.coinGeckoId } ?: token

View file

@ -37,7 +37,7 @@ class FetchCardTokenListUseCase(
} }
val fetchQuotes = async { val fetchQuotes = async {
fetchQuotes( fetchQuotes(
currenciesIds = currencies.mapTo(destination = hashSetOf(), transform = CryptoCurrency::id), currenciesIds = currencies.mapNotNullTo(destination = hashSetOf()) { it.id.rawCurrencyId },
refresh = refresh, refresh = refresh,
) )
} }
@ -79,7 +79,7 @@ class FetchCardTokenListUseCase(
) )
} }
private suspend fun fetchQuotes(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean) { private suspend fun fetchQuotes(currenciesIds: Set<CryptoCurrency.RawID>, refresh: Boolean) {
catch( catch(
block = { quotesRepository.getQuotesSync(currenciesIds, refresh) }, block = { quotesRepository.getQuotesSync(currenciesIds, refresh) },
catch = { /* Ignore error */ }, catch = { /* Ignore error */ },

View file

@ -125,7 +125,7 @@ class FetchCurrencyStatusUseCase(
private suspend fun Raise<CurrencyStatusError>.fetchQuote(currencyId: CryptoCurrency.ID, refresh: Boolean) { private suspend fun Raise<CurrencyStatusError>.fetchQuote(currencyId: CryptoCurrency.ID, refresh: Boolean) {
catch( catch(
block = { quotesRepository.getQuotesSync(setOf(currencyId), refresh) }, block = { quotesRepository.getQuotesSync(setOfNotNull(currencyId.rawCurrencyId), refresh) },
) { ) {
raise(CurrencyStatusError.DataError(it)) raise(CurrencyStatusError.DataError(it))
} }

View file

@ -105,7 +105,10 @@ class FetchTokenListUseCase(
private suspend fun fetchQuotes(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean) { private suspend fun fetchQuotes(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean) {
catch( catch(
block = { quotesRepository.getQuotesSync(currenciesIds, refresh) }, block = {
val rawIds = currenciesIds.mapNotNull { it.rawCurrencyId }.toSet()
quotesRepository.getQuotesSync(rawIds, refresh)
},
) { ) {
/* Ignore error */ /* Ignore error */
} }

View file

@ -41,7 +41,7 @@ class GetAllWalletsCryptoCurrencyStatusesUseCase(
*/ */
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
operator fun invoke( operator fun invoke(
currencyRawId: String, currencyRawId: CryptoCurrency.RawID,
): Flow<Map<UserWallet, List<Either<CurrencyStatusError, CryptoCurrencyStatus>>>> { ): Flow<Map<UserWallet, List<Either<CurrencyStatusError, CryptoCurrencyStatus>>>> {
return currenciesRepository.getAllWalletsCryptoCurrencies(currencyRawId) return currenciesRepository.getAllWalletsCryptoCurrencies(currencyRawId)
.flatMapLatest { userWalletsWithCurrencies: Map<UserWallet, List<CryptoCurrency>> -> .flatMapLatest { userWalletsWithCurrencies: Map<UserWallet, List<CryptoCurrency>> ->

View file

@ -47,7 +47,7 @@ class RefreshMultiCurrencyWalletQuotesUseCase(
private suspend fun fetchQuotes(currenciesIds: Set<CryptoCurrency.ID>) { private suspend fun fetchQuotes(currenciesIds: Set<CryptoCurrency.ID>) {
catch( catch(
block = { block = {
quotesRepository.fetchQuotes(currenciesIds) quotesRepository.fetchQuotes(currenciesIds.mapNotNullTo(hashSetOf(), CryptoCurrency.ID::rawCurrencyId))
}, },
catch = { /* Ignore error */ }, catch = { /* Ignore error */ },
) )

View file

@ -166,7 +166,7 @@ internal class CurrenciesStatusesLceOperations(
} }
private fun getQuotes(tokensIds: NonEmptySet<CryptoCurrency.ID>): Flow<Either<TokenListError, Set<Quote>>> { private fun getQuotes(tokensIds: NonEmptySet<CryptoCurrency.ID>): Flow<Either<TokenListError, Set<Quote>>> {
return quotesRepository.getQuotesUpdates(tokensIds) return quotesRepository.getQuotesUpdates(tokensIds.mapNotNull { it.rawCurrencyId }.toSet())
.map<Set<Quote>, Either<TokenListError, Set<Quote>>> { it.right() } .map<Set<Quote>, Either<TokenListError, Set<Quote>>> { it.right() }
.retryWhen { cause, _ -> .retryWhen { cause, _ ->
emit(TokenListError.DataError(cause).left()) emit(TokenListError.DataError(cause).left())

View file

@ -31,7 +31,8 @@ internal class CurrenciesStatusesOperations(
currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId).toNonEmptyListOrNull() currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId).toNonEmptyListOrNull()
?: return emptyList<CryptoCurrencyStatus>().right() ?: return emptyList<CryptoCurrencyStatus>().right()
val (networks, currenciesIds) = getIds(nonEmptyCurrencies) val (networks, currenciesIds) = getIds(nonEmptyCurrencies)
val quotes = quotesRepository.getQuotesSync(currenciesIds, false).right() val rawIds = currenciesIds.mapNotNull { it.rawCurrencyId }.toSet()
val quotes = quotesRepository.getQuotesSync(rawIds, false).right()
val networkStatuses = val networkStatuses =
networksRepository.getNetworkStatusesSync(userWalletId, networks, false).right() networksRepository.getNetworkStatusesSync(userWalletId, networks, false).right()
val yieldBalances = getYieldBalancesSync(nonEmptyCurrencies) val yieldBalances = getYieldBalancesSync(nonEmptyCurrencies)
@ -60,7 +61,8 @@ internal class CurrenciesStatusesOperations(
} else { } else {
currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, cryptoCurrencyId) currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, cryptoCurrencyId)
} }
val quotes = quotesRepository.getQuoteSync(cryptoCurrencyId)?.right() ?: Error.EmptyQuotes.left() val quote = cryptoCurrencyId.rawCurrencyId?.let { quotesRepository.getQuoteSync(it) }?.right()
?: Error.EmptyQuotes.left()
val networkStatuses = val networkStatuses =
networksRepository.getNetworkStatusesSync( networksRepository.getNetworkStatusesSync(
userWalletId, userWalletId,
@ -71,7 +73,7 @@ internal class CurrenciesStatusesOperations(
}.right() }.right()
val yieldBalances = getYieldBalanceSync(currency) val yieldBalances = getYieldBalanceSync(currency)
return createCurrencyStatus(currency, quotes, networkStatuses, yieldBalances) return createCurrencyStatus(currency, quote, networkStatuses, yieldBalances)
}, },
catch = { raise(Error.DataError(it)) }, catch = { raise(Error.DataError(it)) },
) )
@ -104,7 +106,10 @@ internal class CurrenciesStatusesOperations(
catch = { raise(Error.DataError(it)) }, catch = { raise(Error.DataError(it)) },
) )
val quotes = catch( val quotes = catch(
block = { quotesRepository.getQuoteSync(currency.id)?.right() ?: Error.EmptyQuotes.left() }, block = {
currency.id.rawCurrencyId?.let { quotesRepository.getQuoteSync(it) }
?.right() ?: Error.EmptyQuotes.left()
},
catch = { Error.DataError(it).left() }, catch = { Error.DataError(it).left() },
) )
val networkStatus = catch( val networkStatus = catch(
@ -176,8 +181,7 @@ internal class CurrenciesStatusesOperations(
fun getCurrencyStatusFlow( fun getCurrencyStatusFlow(
currency: CryptoCurrency, currency: CryptoCurrency,
includeQuotes: Boolean = true, includeQuotes: Boolean = true,
): Flow<Either<Error, ): Flow<Either<Error, CryptoCurrencyStatus>> {
CryptoCurrencyStatus,>,> {
val (networks, currenciesIds) = getIds(nonEmptyListOf(currency)) val (networks, currenciesIds) = getIds(nonEmptyListOf(currency))
val quoteFlow = if (includeQuotes) { val quoteFlow = if (includeQuotes) {
@ -344,7 +348,8 @@ internal class CurrenciesStatusesOperations(
} }
private fun getQuotes(tokensIds: NonEmptySet<CryptoCurrency.ID>): Flow<Either<Error, Set<Quote>>> { private fun getQuotes(tokensIds: NonEmptySet<CryptoCurrency.ID>): Flow<Either<Error, Set<Quote>>> {
return quotesRepository.getQuotesUpdates(tokensIds) val rawIds = tokensIds.mapNotNull { it.rawCurrencyId }.toSet()
return quotesRepository.getQuotesUpdates(rawIds)
.map<Set<Quote>, Either<Error, Set<Quote>>> { quotes -> .map<Set<Quote>, Either<Error, Set<Quote>>> { quotes ->
if (quotes.isEmpty()) Error.EmptyQuotes.left() else quotes.right() if (quotes.isEmpty()) Error.EmptyQuotes.left() else quotes.right()
} }

View file

@ -74,7 +74,7 @@ internal class CurrencyStatusOperations(
val yieldBalanceData = yieldBalance as? YieldBalance.Data val yieldBalanceData = yieldBalance as? YieldBalance.Data
val isCurrentAddressStaking = yieldBalanceData?.address == status.address.defaultAddress.value val isCurrentAddressStaking = yieldBalanceData?.address == status.address.defaultAddress.value
val filteredTokenBalances = yieldBalanceData?.balance?.items?.filter { val filteredTokenBalances = yieldBalanceData?.balance?.items?.filter {
it.token.coinGeckoId == currency.id.rawCurrencyId it.token.coinGeckoId == currency.id.rawCurrencyId?.value
} }
val currentYieldBalance = if (isCurrentAddressStaking && filteredTokenBalances?.isNotEmpty() == true) { val currentYieldBalance = if (isCurrentAddressStaking && filteredTokenBalances?.isNotEmpty() == true) {
yieldBalanceData.copy( yieldBalanceData.copy(

View file

@ -248,7 +248,7 @@ interface CurrenciesRepository {
): CryptoCurrency.Token ): CryptoCurrency.Token
/** Get crypto currencies by [currencyRawId] from all user wallets */ /** Get crypto currencies by [currencyRawId] from all user wallets */
fun getAllWalletsCryptoCurrencies(currencyRawId: String): Flow<Map<UserWallet, List<CryptoCurrency>>> fun getAllWalletsCryptoCurrencies(currencyRawId: CryptoCurrency.RawID): Flow<Map<UserWallet, List<CryptoCurrency>>>
fun isNetworkFeeZero(userWalletId: UserWalletId, network: Network): Boolean fun isNetworkFeeZero(userWalletId: UserWalletId, network: Network): Boolean
} }

View file

@ -17,7 +17,7 @@ interface QuotesRepository {
* @param currenciesIds The unique identifiers of the cryptocurrencies for which quotes are to be retrieved. * @param currenciesIds The unique identifiers of the cryptocurrencies for which quotes are to be retrieved.
* @return A [Flow] emitting a set of quotes corresponding to the specified cryptocurrencies. * @return A [Flow] emitting a set of quotes corresponding to the specified cryptocurrencies.
*/ */
fun getQuotesUpdates(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean = false): Flow<Set<Quote>> fun getQuotesUpdates(currenciesIds: Set<CryptoCurrency.RawID>, refresh: Boolean = false): Flow<Set<Quote>>
/** /**
* Retrieves quotes for a set of specified cryptocurrencies, identified by their unique IDs. * Retrieves quotes for a set of specified cryptocurrencies, identified by their unique IDs.
@ -28,9 +28,9 @@ interface QuotesRepository {
* @param refresh A boolean flag indicating whether the data should be refreshed. * @param refresh A boolean flag indicating whether the data should be refreshed.
* @return A [Flow] emitting a set of quotes corresponding to the specified cryptocurrencies. * @return A [Flow] emitting a set of quotes corresponding to the specified cryptocurrencies.
*/ */
suspend fun getQuotesSync(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Set<Quote> suspend fun getQuotesSync(currenciesIds: Set<CryptoCurrency.RawID>, refresh: Boolean): Set<Quote>
suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote? suspend fun getQuoteSync(currencyId: CryptoCurrency.RawID): Quote?
suspend fun fetchQuotes(currenciesIds: Set<CryptoCurrency.ID>) suspend fun fetchQuotes(currenciesIds: Set<CryptoCurrency.RawID>)
} }

View file

@ -1,6 +1,7 @@
package com.tangem.domain.tokens.mock package com.tangem.domain.tokens.mock
import arrow.core.nonEmptySetOf import arrow.core.nonEmptySetOf
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.model.Quote
import java.math.BigDecimal import java.math.BigDecimal
@ -67,11 +68,10 @@ internal object MockQuotes {
priceChange = BigDecimal("-0.10"), priceChange = BigDecimal("-0.10"),
) )
val quote11 = Quote.Empty(null) val quote11 = Quote.Empty(CryptoCurrency.RawID("null"))
val quote12 = Quote.Empty("null")
val quotes = nonEmptySetOf( val quotes = nonEmptySetOf(
quote1, quote2, quote3, quote4, quote5, quote6, quote7, quote8, quote9, quote10, quote1, quote2, quote3, quote4, quote5, quote6, quote7, quote8, quote9, quote10,
quote11, quote12, quote11,
) )
} }

View file

@ -153,7 +153,9 @@ internal class MockCurrenciesRepository(
error("not implemented") error("not implemented")
} }
override fun getAllWalletsCryptoCurrencies(currencyRawId: String): Flow<Map<UserWallet, List<CryptoCurrency>>> { override fun getAllWalletsCryptoCurrencies(
currencyRawId: CryptoCurrency.RawID,
): Flow<Map<UserWallet, List<CryptoCurrency>>> {
return emptyFlow() return emptyFlow()
} }

View file

@ -13,18 +13,18 @@ internal class MockQuotesRepository(
private val quotes: Flow<Either<DataError, Set<Quote>>>, private val quotes: Flow<Either<DataError, Set<Quote>>>,
) : QuotesRepository { ) : QuotesRepository {
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 quotes.map { it.getOrElse { e -> throw e } } return quotes.map { it.getOrElse { e -> throw e } }
} }
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 getQuotesUpdates(currenciesIds).first() return getQuotesUpdates(currenciesIds).first()
} }
override suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote { override suspend fun getQuoteSync(currencyId: CryptoCurrency.RawID): Quote {
return quotes.map { it.getOrElse { e -> throw e } }.first() return quotes.map { it.getOrElse { e -> throw e } }.first()
.first { it.rawCurrencyId == currencyId.rawCurrencyId } .first { it.rawCurrencyId == currencyId }
} }
override suspend fun fetchQuotes(currenciesIds: Set<CryptoCurrency.ID>) {} override suspend fun fetchQuotes(currenciesIds: Set<CryptoCurrency.RawID>) {}
} }

View file

@ -13,6 +13,7 @@ import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.markets.details.MarketsTokenDetailsComponent import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.details.MarketsTokenDetailsComponent.Params import com.tangem.features.markets.details.MarketsTokenDetailsComponent.Params
import com.tangem.features.markets.details.impl.analytics.MarketDetailsAnalyticsEvent import com.tangem.features.markets.details.impl.analytics.MarketDetailsAnalyticsEvent
@ -38,7 +39,7 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
// applying l2 compatibility // applying l2 compatibility
private val updatedParams = params.copy( private val updatedParams = params.copy(
token = params.token.copy( token = params.token.copy(
id = getTokenIdIfL2Network(params.token.id), id = CryptoCurrency.RawID(getTokenIdIfL2Network(params.token.id.value)),
), ),
) )
private val analyticsParams = params.analyticsParams private val analyticsParams = params.analyticsParams

View file

@ -134,7 +134,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
modelScope.launch { modelScope.launch {
sendFeedbackEmailUseCase( sendFeedbackEmailUseCase(
type = FeedbackEmailType.CurrencyDescriptionError( type = FeedbackEmailType.CurrencyDescriptionError(
currencyId = params.token.id, currencyId = params.token.id.value,
currencyName = params.token.name, currencyName = params.token.name,
), ),
) )

View file

@ -9,6 +9,7 @@ import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase
import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletId
@ -37,7 +38,7 @@ internal class PortfolioDataLoader @Inject constructor(
/** Load data by [currencyRawId] */ /** Load data by [currencyRawId] */
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
fun load(currencyRawId: String): Flow<PortfolioData> { fun load(currencyRawId: CryptoCurrency.RawID): Flow<PortfolioData> {
return combine( return combine(
flow = getAllWalletsCryptoCurrenciesData(currencyRawId = currencyRawId), flow = getAllWalletsCryptoCurrenciesData(currencyRawId = currencyRawId),
flow2 = getSelectedAppCurrencyFlow(), flow2 = getSelectedAppCurrencyFlow(),
@ -62,7 +63,7 @@ internal class PortfolioDataLoader @Inject constructor(
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
private fun getAllWalletsCryptoCurrenciesData( private fun getAllWalletsCryptoCurrenciesData(
currencyRawId: String, currencyRawId: CryptoCurrency.RawID,
): Flow<Map<UserWallet, List<PortfolioData.CryptoCurrencyData>>> { ): Flow<Map<UserWallet, List<PortfolioData.CryptoCurrencyData>>> {
return getAllWalletsCryptoCurrencyStatusesUseCase(currencyRawId) return getAllWalletsCryptoCurrencyStatusesUseCase(currencyRawId)
.distinctUntilChanged() .distinctUntilChanged()

View file

@ -24,7 +24,7 @@ internal class SelectNetworkUMConverter(
override fun convert(value: TokenMarketParams): SelectNetworkUM { override fun convert(value: TokenMarketParams): SelectNetworkUM {
return SelectNetworkUM( return SelectNetworkUM(
tokenId = value.id, tokenId = value.id.value,
iconUrl = value.imageUrl, iconUrl = value.imageUrl,
tokenName = value.name, tokenName = value.name,
tokenCurrencySymbol = value.symbol, tokenCurrencySymbol = value.symbol,

View file

@ -9,6 +9,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
import com.tangem.domain.markets.TokenMarket import com.tangem.domain.markets.TokenMarket
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.tokenlist.impl.analytics.MarketsListAnalyticsEvent import com.tangem.features.markets.tokenlist.impl.analytics.MarketsListAnalyticsEvent
import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListBatchFlowManager import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListBatchFlowManager
@ -48,7 +49,7 @@ internal class MarketsListModel @Inject constructor(
initialValue = AppCurrency.Default, initialValue = AppCurrency.Default,
) )
private val visibleItemIds = MutableStateFlow<List<String>>(emptyList()) private val visibleItemIds = MutableStateFlow<List<CryptoCurrency.RawID>>(emptyList())
private val marketsListUMStateManager = MarketsListUMStateManager( private val marketsListUMStateManager = MarketsListUMStateManager(
currentVisibleIds = Provider { visibleItemIds.value }, currentVisibleIds = Provider { visibleItemIds.value },
@ -72,8 +73,8 @@ internal class MarketsListModel @Inject constructor(
getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase,
batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search, batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search,
currentAppCurrency = Provider { currentAppCurrency.value }, currentAppCurrency = Provider { currentAppCurrency.value },
currentTrendInterval = Provider { marketsListUMStateManager.selectedInterval }, // FIXME fix on backend currentTrendInterval = Provider { marketsListUMStateManager.selectedInterval },
currentSortByType = Provider { SortByTypeUM.Rating }, // FIXME maybe fix on backend currentSortByType = Provider { SortByTypeUM.Rating },
currentSearchText = Provider { marketsListUMStateManager.searchQuery }, currentSearchText = Provider { marketsListUMStateManager.searchQuery },
modelScope = modelScope, modelScope = modelScope,
dispatchers = dispatchers, dispatchers = dispatchers,

View file

@ -2,6 +2,7 @@ package com.tangem.features.markets.tokenlist.impl.model.statemanager
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.* import com.tangem.domain.markets.*
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.markets.tokenlist.impl.model.converters.MarketsTokenItemConverter import com.tangem.features.markets.tokenlist.impl.model.converters.MarketsTokenItemConverter
import com.tangem.features.markets.tokenlist.impl.model.utils.logAction import com.tangem.features.markets.tokenlist.impl.model.utils.logAction
import com.tangem.features.markets.tokenlist.impl.model.utils.logStatus import com.tangem.features.markets.tokenlist.impl.model.utils.logStatus
@ -302,7 +303,7 @@ internal class MarketsListBatchFlowManager(
} }
} }
fun getBatchKeysByItemIds(ids: List<String>): Set<Int> { fun getBatchKeysByItemIds(ids: List<CryptoCurrency.RawID>): Set<Int> {
val currentData = batchFlow.state.value.data val currentData = batchFlow.state.value.data
return currentData return currentData
@ -311,7 +312,7 @@ internal class MarketsListBatchFlowManager(
.toSet() .toSet()
} }
fun getTokenById(id: String): TokenMarket? { fun getTokenById(id: CryptoCurrency.RawID): TokenMarket? {
return batchFlow.state.value.data.map { it.data }.flatten().find { it.id == id } return batchFlow.state.value.data.map { it.data }.flatten().find { it.id == id }
} }

View file

@ -6,6 +6,7 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.markets.impl.R import com.tangem.features.markets.impl.R
import com.tangem.features.markets.tokenlist.impl.ui.state.* import com.tangem.features.markets.tokenlist.impl.ui.state.*
import com.tangem.utils.Provider import com.tangem.utils.Provider
@ -18,9 +19,9 @@ import kotlinx.coroutines.flow.update
@Stable @Stable
internal class MarketsListUMStateManager( internal class MarketsListUMStateManager(
private val currentVisibleIds: Provider<List<String>>, private val currentVisibleIds: Provider<List<CryptoCurrency.RawID>>,
private val onLoadMoreUiItems: () -> Unit, private val onLoadMoreUiItems: () -> Unit,
private val visibleItemsChanged: (itemsKeys: List<String>) -> Unit, private val visibleItemsChanged: (itemsKeys: List<CryptoCurrency.RawID>) -> Unit,
private val onRetryButtonClicked: () -> Unit, private val onRetryButtonClicked: () -> Unit,
private val onTokenClick: (MarketsListItemUM) -> Unit, private val onTokenClick: (MarketsListItemUM) -> Unit,
) { ) {

View file

@ -36,6 +36,7 @@ import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.impl.R import com.tangem.features.markets.impl.R
import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListLazyColumn import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListLazyColumn
@ -298,7 +299,7 @@ private fun Preview() {
items = MarketChartListItemPreviewDataProvider().values items = MarketChartListItemPreviewDataProvider().values
.flatMap { item -> List(size = 10) { item } } .flatMap { item -> List(size = 10) { item } }
.mapIndexed { index, item -> .mapIndexed { index, item ->
item.copy(id = index.toString()) item.copy(id = CryptoCurrency.RawID(index.toString()))
} }
.toImmutableList(), .toImmutableList(),
showUnder100kTokensNotification = false, showUnder100kTokensNotification = false,

View file

@ -17,6 +17,7 @@ import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.markets.impl.R import com.tangem.features.markets.impl.R
import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -88,7 +89,7 @@ internal fun MarketsListLazyColumn(
is ListUM.Content -> { is ListUM.Content -> {
items( items(
items = state.items, items = state.items,
key = { it.id + TOKEN_LAZY_LIST_ID_SEPARATOR + it.marketCap.toString() }, key = { it.id.value + TOKEN_LAZY_LIST_ID_SEPARATOR + it.marketCap.toString() },
) { item -> ) { item ->
MarketsListItem( MarketsListItem(
model = item, model = item,
@ -193,6 +194,7 @@ private fun VisibleItemsTracker(listState: LazyListState, state: ListUM) {
derivedStateOf { derivedStateOf {
listState.layoutInfo.visibleItemsInfo.mapNotNull { listState.layoutInfo.visibleItemsInfo.mapNotNull {
(it.key as? String)?.split(TOKEN_LAZY_LIST_ID_SEPARATOR)?.first() (it.key as? String)?.split(TOKEN_LAZY_LIST_ID_SEPARATOR)?.first()
?.let { rawId -> CryptoCurrency.RawID(rawId) }
} }
} }
} }

View file

@ -4,13 +4,14 @@ package com.tangem.features.markets.tokenlist.impl.ui.preview
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider<MarketsListItemUM>( internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider<MarketsListItemUM>(
collection = listOf( collection = listOf(
MarketsListItemUM( MarketsListItemUM(
id = "1", id = CryptoCurrency.RawID("1"),
name = "Bitcoin", name = "Bitcoin",
currencySymbol = "BTC", currencySymbol = "BTC",
iconUrl = "", iconUrl = "",
@ -25,7 +26,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
isUnder100kMarketCap = false, isUnder100kMarketCap = false,
), ),
MarketsListItemUM( MarketsListItemUM(
id = "1", id = CryptoCurrency.RawID("1"),
name = "Bitcoin", name = "Bitcoin",
currencySymbol = "BTC", currencySymbol = "BTC",
iconUrl = null, iconUrl = null,
@ -38,7 +39,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
isUnder100kMarketCap = false, isUnder100kMarketCap = false,
), ),
MarketsListItemUM( MarketsListItemUM(
id = "1", id = CryptoCurrency.RawID("1"),
name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin", name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin",
currencySymbol = "BTC", currencySymbol = "BTC",
iconUrl = null, iconUrl = null,
@ -53,7 +54,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
isUnder100kMarketCap = false, isUnder100kMarketCap = false,
), ),
MarketsListItemUM( MarketsListItemUM(
id = "1", id = CryptoCurrency.RawID("1"),
name = "Bitcoin", name = "Bitcoin",
currencySymbol = "BTC", currencySymbol = "BTC",
iconUrl = null, iconUrl = null,
@ -68,7 +69,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
isUnder100kMarketCap = false, isUnder100kMarketCap = false,
), ),
MarketsListItemUM( MarketsListItemUM(
id = "1", id = CryptoCurrency.RawID("1"),
name = "Bitcoin", name = "Bitcoin",
currencySymbol = "BTC", currencySymbol = "BTC",
iconUrl = null, iconUrl = null,
@ -83,7 +84,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
isUnder100kMarketCap = false, isUnder100kMarketCap = false,
), ),
MarketsListItemUM( MarketsListItemUM(
id = "1", id = CryptoCurrency.RawID("1"),
name = "Bitcoin", name = "Bitcoin",
currencySymbol = "BTC", currencySymbol = "BTC",
iconUrl = null, iconUrl = null,

View file

@ -4,10 +4,11 @@ import androidx.compose.runtime.Immutable
import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.common.ui.charts.state.MarketChartLook
import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.domain.tokens.model.CryptoCurrency
@Immutable @Immutable
data class MarketsListItemUM( data class MarketsListItemUM(
val id: String, val id: CryptoCurrency.RawID,
val name: String, val name: String,
val currencySymbol: String, val currencySymbol: String,
val iconUrl: String?, val iconUrl: String?,

View file

@ -6,6 +6,7 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.markets.impl.R import com.tangem.features.markets.impl.R
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -44,7 +45,7 @@ sealed class ListUM {
val showUnder100kTokensNotification: Boolean, val showUnder100kTokensNotification: Boolean,
val showUnder100kTokensNotificationWasHidden: Boolean, val showUnder100kTokensNotificationWasHidden: Boolean,
val loadMore: () -> Unit, val loadMore: () -> Unit,
val visibleIdsChanged: (List<String>) -> Unit, val visibleIdsChanged: (List<CryptoCurrency.RawID>) -> Unit,
val onShowTokensUnder100kClicked: () -> Unit, val onShowTokensUnder100kClicked: () -> Unit,
val triggerScrollReset: StateEvent<Unit>, val triggerScrollReset: StateEvent<Unit>,
val onItemClick: (MarketsListItemUM) -> Unit, val onItemClick: (MarketsListItemUM) -> Unit,

View file

@ -229,7 +229,7 @@ internal class FeeStateFactory(
symbol = this.token.symbol, symbol = this.token.symbol,
contractAddress = this.token.contractAddress, contractAddress = this.token.contractAddress,
decimals = this.token.decimals, decimals = this.token.decimals,
id = this.token.id.rawCurrencyId, id = this.token.id.rawCurrencyId?.value,
), ),
) )
} }

View file

@ -89,7 +89,7 @@ internal class RewardsValidatorStateConverter(
formattedCryptoAmount = cryptoAmount, formattedCryptoAmount = cryptoAmount,
fiatAmount = fiatValue, fiatAmount = fiatValue,
formattedFiatAmount = formattedFiatAmount, formattedFiatAmount = formattedFiatAmount,
rawCurrencyId = cryptoCurrency.id.rawCurrencyId, rawCurrencyId = cryptoCurrency.id.rawCurrencyId?.value,
pendingActions = balance.pendingActions.toPersistentList(), pendingActions = balance.pendingActions.toPersistentList(),
isClickable = true, isClickable = true,
type = balance.type, type = balance.type,

View file

@ -15,6 +15,7 @@ import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.staking.getCurrentToken
import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWallet

View file

@ -18,6 +18,7 @@ import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionStatus import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionStatus
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType
import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.staking.getCurrentToken
import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase

View file

@ -2134,14 +2134,17 @@ internal class SwapInteractorImpl @AssistedInject constructor(
} }
private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map<CryptoCurrency.ID, Quote.Value> { private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map<CryptoCurrency.ID, Quote.Value> {
val set = ids.toSet().getQuotesOrEmpty(false).filterIsInstance<Quote.Value>() val set = ids.mapNotNull { it.rawCurrencyId }
.toSet()
.getQuotesOrEmpty(false)
.filterIsInstance<Quote.Value>()
return ids return ids
.mapNotNull { id -> set.find { it.rawCurrencyId == id.rawCurrencyId }?.let { id to it } } .mapNotNull { id -> set.find { it.rawCurrencyId == id.rawCurrencyId }?.let { id to it } }
.toMap() .toMap()
} }
private suspend fun Set<CryptoCurrency.ID>.getQuotesOrEmpty(refresh: Boolean): Set<Quote> { private suspend fun Set<CryptoCurrency.RawID>.getQuotesOrEmpty(refresh: Boolean): Set<Quote> {
return try { return try {
quotesRepository.getQuotesSync(this, refresh) quotesRepository.getQuotesSync(this, refresh)
} catch (t: Throwable) { } catch (t: Throwable) {

View file

@ -205,7 +205,8 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
private suspend fun Set<CryptoCurrency.ID>.getQuotesOrEmpty(refresh: Boolean): Set<Quote> { private suspend fun Set<CryptoCurrency.ID>.getQuotesOrEmpty(refresh: Boolean): Set<Quote> {
return try { return try {
quotesRepository.getQuotesSync(this, refresh) val rawIds = mapNotNull { it.rawCurrencyId }.toSet()
quotesRepository.getQuotesSync(rawIds, refresh)
} catch (t: Throwable) { } catch (t: Throwable) {
emptySet() emptySet()
} }