Updated on 2026-08-14
This commit is contained in:
commit
94703ae459
14 changed files with 302 additions and 15 deletions
|
|
@ -2,10 +2,7 @@ package com.tangem.tap.di.domain
|
|||
|
||||
import com.tangem.domain.exchange.RampStateManager
|
||||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
|
||||
import com.tangem.domain.tokens.repository.NetworksRepository
|
||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
import com.tangem.domain.tokens.repository.*
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -231,4 +228,10 @@ internal object TokensDomainModule {
|
|||
): GetMissedAddressesCryptoCurrenciesUseCase {
|
||||
return GetMissedAddressesCryptoCurrenciesUseCase(currenciesRepository = currenciesRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideGetGlobalTokenListUseCase(tokensListRepository: TokensListRepository): GetGlobalTokenListUseCase {
|
||||
return GetGlobalTokenListUseCase(repository = tokensListRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,16 @@ fun CryptoCurrency.Token.tryGetBackgroundForTokenIcon(
|
|||
): Color {
|
||||
if (isGrayscale) return TangemColorPalette.Dark2
|
||||
|
||||
return tryGetBackgroundForTokenIcon(contractAddress = contractAddress, fallbackColor = fallbackColor)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to extract a background color from the contract address of a token.
|
||||
*
|
||||
* @param fallbackColor The color to use as a fallback.
|
||||
* @return The extracted background color or the fallback color if extraction fails or if it is a test network token.
|
||||
*/
|
||||
fun tryGetBackgroundForTokenIcon(contractAddress: String, fallbackColor: Color = TangemColorPalette.Black): Color {
|
||||
return try {
|
||||
val colorHex = "#" + contractAddress.substring(range = COLOR_HEX_START_INDEX..COLOR_HEX_END_INDEX)
|
||||
Color(colorHex.toColorInt())
|
||||
|
|
|
|||
|
|
@ -45,4 +45,5 @@ dependencies {
|
|||
implementation(deps.jodatime)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.retrofit) // For HttpException
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
}
|
||||
|
|
@ -1,10 +1,7 @@
|
|||
package com.tangem.data.tokens.di
|
||||
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.tokens.repository.DefaultCurrenciesRepository
|
||||
import com.tangem.data.tokens.repository.DefaultMarketCryptoCurrencyRepository
|
||||
import com.tangem.data.tokens.repository.DefaultNetworksRepository
|
||||
import com.tangem.data.tokens.repository.DefaultQuotesRepository
|
||||
import com.tangem.data.tokens.repository.*
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.network.NetworksStatusesStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
|
|
@ -12,10 +9,7 @@ import com.tangem.datasource.local.quote.QuotesStore
|
|||
import com.tangem.datasource.local.token.UserMarketCoinsStore
|
||||
import com.tangem.datasource.local.token.UserTokensStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
|
||||
import com.tangem.domain.tokens.repository.NetworksRepository
|
||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
import com.tangem.domain.tokens.repository.*
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -93,4 +87,18 @@ internal object TokensDataModule {
|
|||
): MarketCryptoCurrencyRepository {
|
||||
return DefaultMarketCryptoCurrencyRepository(userMarketCoinsStore)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesTokensListRepository(
|
||||
tangemTechApi: TangemTechApi,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
quotesRepository: QuotesRepository,
|
||||
): TokensListRepository {
|
||||
return DefaultTokensListRepository(
|
||||
tangemTechApi = tangemTechApi,
|
||||
dispatchers = dispatchers,
|
||||
quotesRepository = quotesRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package com.tangem.data.tokens.paging
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
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,
|
||||
)
|
||||
}.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),
|
||||
)
|
||||
}
|
||||
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),
|
||||
)
|
||||
},
|
||||
onFailure = { LoadResult.Error(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class CoinsData(
|
||||
val coins: List<CoinsResponse.Coin>,
|
||||
val imageHost: String?,
|
||||
val quotes: Set<Quote>,
|
||||
)
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.tangem.data.tokens.paging
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.data.tokens.utils.getNetworkStandardType
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
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,
|
||||
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/"
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.data.tokens.repository
|
||||
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.data.tokens.paging.CoinsPagingSource
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.domain.tokens.model.Token
|
||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
import com.tangem.domain.tokens.repository.TokensListRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Default repository implementation for managing operations related to a complete set of tokens
|
||||
*
|
||||
* @property tangemTechApi Tangem Tech API
|
||||
* @property dispatchers coroutine dispatchers provider
|
||||
* @property quotesRepository responsible for providing cryptocurrency quotes data
|
||||
*
|
||||
*/
|
||||
internal class DefaultTokensListRepository(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val quotesRepository: QuotesRepository,
|
||||
) : TokensListRepository {
|
||||
|
||||
override fun getTokens(searchText: String?): Flow<PagingData<Token>> {
|
||||
return Pager(
|
||||
config = PagingConfig(
|
||||
pageSize = 100,
|
||||
prefetchDistance = 70,
|
||||
enablePlaceholders = false,
|
||||
),
|
||||
pagingSourceFactory = {
|
||||
CoinsPagingSource(
|
||||
api = tangemTechApi,
|
||||
dispatchers = dispatchers,
|
||||
searchText = searchText,
|
||||
quotesRepository = quotesRepository,
|
||||
)
|
||||
},
|
||||
).flow
|
||||
}
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ private fun getNetworkDerivationPath(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType {
|
||||
internal fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType {
|
||||
return when (blockchain) {
|
||||
Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.StandardType.ERC20
|
||||
Blockchain.BSC, Blockchain.BSCTestnet -> Network.StandardType.BEP20
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ dependencies {
|
|||
/** Project - Other */
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/** Android - Other */
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
|
||||
/** Utils */
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.reKotlin)
|
||||
|
|
|
|||
|
|
@ -3,14 +3,17 @@ package com.tangem.domain.tokens.model
|
|||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Represents a financial quote 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.
|
||||
*
|
||||
* @property rawCurrencyId The unique identifier of the token for which the quote is provided.
|
||||
* @property rawCurrencyId The unique identifier of the cryptocurrency for which the financial information is provided.
|
||||
* @property fiatRate The current fiat exchange rate for the cryptocurrency.
|
||||
* @property priceChange The price change for the cryptocurrency.
|
||||
* @property values The values representing the cryptocurrency's price changes over a 24-hour period,
|
||||
* suitable for chart plotting.
|
||||
*/
|
||||
data class Quote(
|
||||
val rawCurrencyId: String,
|
||||
val fiatRate: BigDecimal,
|
||||
val priceChange: BigDecimal,
|
||||
val values: List<Double>? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.domain.tokens.model
|
||||
|
||||
/**
|
||||
* Represents a domain model for a token (as part of a general list of tokens).
|
||||
*
|
||||
* @property id The unique identifier of the token.
|
||||
* @property networks List of networks associated with the token.
|
||||
* @property isAvailable Indicates whether this token is supported in Tangem app.
|
||||
* @property quote Equivalent prices for the token.
|
||||
* @property name The name of the token.
|
||||
* @property symbol The brief name of the token, e.g., "BTC".
|
||||
* @property iconUrl URL of the token's icon.
|
||||
*/
|
||||
data class Token(
|
||||
val id: String,
|
||||
val networks: List<Network>,
|
||||
val isAvailable: Boolean,
|
||||
val quote: Quote?,
|
||||
val name: String,
|
||||
val symbol: String,
|
||||
val iconUrl: String,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Represents a domain model for a network associated with a token.
|
||||
*
|
||||
* @property networkId The unique identifier of the network.
|
||||
* @property standardType The type of blockchain associated with the network.
|
||||
* @property address The contract address of the token on the current network.
|
||||
* It is null for the main currencies of the network.
|
||||
* @property iconUrl URL of the network's icon.
|
||||
* @property decimalCount The decimal count associated with the token on the network.
|
||||
*/
|
||||
data class Network(
|
||||
val networkId: String,
|
||||
val standardType: String,
|
||||
val address: String?,
|
||||
val iconUrl: String,
|
||||
val decimalCount: Int?,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.domain.tokens.model.Token
|
||||
import com.tangem.domain.tokens.repository.TokensListRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Use case for retrieving a complete set of tokens with their quotes (filtered by a search text if provided).
|
||||
*
|
||||
* @property repository The repository responsible for fetching and preparing the list of tokens with their quotes.
|
||||
*/
|
||||
class GetGlobalTokenListUseCase(private val repository: TokensListRepository) {
|
||||
|
||||
/**
|
||||
* @param searchText The text used for filtering the list of tokens (not required).
|
||||
* @return A [Flow] emitting [PagingData] containing the tokens with their quotes.
|
||||
*/
|
||||
operator fun invoke(searchText: String?): Flow<PagingData<Token>> {
|
||||
return repository.getTokens(searchText = searchText?.ifBlank(defaultValue = { null }))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.domain.tokens.repository
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.domain.tokens.model.Token
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Repository interface for managing operations related to a complete set of tokens
|
||||
* (not associated with any specific card).
|
||||
* */
|
||||
interface TokensListRepository {
|
||||
|
||||
/**
|
||||
* Retrieves a list of available tokens with quotes, can be filtered based on the provided search text.
|
||||
*
|
||||
* @param searchText The search text used to filter tokens.
|
||||
* @return A [Flow] emitting [PagingData] containing the tokens with quotes matching the search criteria.
|
||||
*/
|
||||
fun getTokens(searchText: String?): Flow<PagingData<Token>>
|
||||
}
|
||||
|
|
@ -47,6 +47,9 @@ dependencies {
|
|||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/** Project - Data */
|
||||
implementation(projects.data.tokens)
|
||||
|
||||
/** Domain modules */
|
||||
implementation(projects.common)
|
||||
implementation(projects.domain.card)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue