Updated on 2026-08-14

This commit is contained in:
Tangem 2023-08-11 08:26:23 +03:00
parent b284a8195a
commit 9b70e5bc82
14 changed files with 116 additions and 77 deletions

View file

@ -3,6 +3,7 @@ package com.tangem.datasource.local.quote
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.quote.model.StoredQuote
import com.tangem.domain.tokens.models.CryptoCurrency
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
@ -10,9 +11,9 @@ internal class DefaultQuotesStore(
private val dataStore: StringKeyDataStore<StoredQuote>,
) : QuotesStore {
override fun get(rawCurrenciesIds: Set<String>): Flow<Set<StoredQuote>> {
val flows = rawCurrenciesIds.map { rawCurrencyId ->
dataStore.get(rawCurrencyId)
override fun get(currenciesIds: Set<CryptoCurrency.ID>): Flow<Set<StoredQuote>> {
val flows = currenciesIds.mapNotNull { currencyId ->
dataStore.get(currencyId.rawCurrencyId ?: return@mapNotNull null)
}
return combine(flows) { quotes -> quotes.toSet() }

View file

@ -2,11 +2,12 @@ package com.tangem.datasource.local.quote
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.datasource.local.quote.model.StoredQuote
import com.tangem.domain.tokens.models.CryptoCurrency
import kotlinx.coroutines.flow.Flow
interface QuotesStore {
fun get(rawCurrenciesIds: Set<String>): Flow<Set<StoredQuote>>
fun get(currenciesIds: Set<CryptoCurrency.ID>): Flow<Set<StoredQuote>>
suspend fun store(response: QuotesResponse)
}

View file

@ -13,13 +13,13 @@ internal class MockQuotesRepository : QuotesRepository {
override fun getQuotes(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Flow<Set<Quote>> {
return channelFlow {
val quotes = currenciesIds.map {
val quotes = currenciesIds.mapNotNullTo(hashSetOf()) { id ->
Quote(
currencyId = it,
rawCurrencyId = id.rawCurrencyId ?: return@mapNotNullTo null,
fiatRate = BigDecimal.ZERO,
priceChange = BigDecimal.ZERO,
)
}.toSet()
}
delay(Random.nextLong(from = 200, until = 2_000))

View file

@ -39,7 +39,7 @@ internal class NetworkStatusFactory {
is CryptoCurrencyAmount.Coin -> currencies.singleOrNull { it is CryptoCurrency.Coin }
is CryptoCurrencyAmount.Token -> currencies.firstOrNull {
it is CryptoCurrency.Token &&
getTokenIdString(it.id) == amount.id &&
it.id.rawCurrencyId == amount.id &&
it.contractAddress == amount.tokenContractAddress
}
}

View file

@ -13,7 +13,7 @@ import com.tangem.blockchain.common.Token as SdkToken
internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) {
fun createCurrency(currencyId: CryptoCurrency.ID, response: UserTokensResponse, card: CardDTO): CryptoCurrency {
val responseTokenId = getTokenIdString(currencyId)
val responseTokenId = currencyId.rawCurrencyId
val token = requireNotNull(response.tokens.firstOrNull { it.id == responseTokenId }) {
"Unable find a token with provided ID: $responseTokenId"

View file

@ -6,21 +6,21 @@ import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.CryptoCurrency.ID
import com.tangem.domain.tokens.models.Network
import com.tangem.blockchain.common.Token as SdkToken
import com.tangem.domain.tokens.models.CryptoCurrency.ID.Prefix.COIN_PREFIX as COIN_ID_PREFIX
import com.tangem.domain.tokens.models.CryptoCurrency.ID.Prefix.CUSTOM_TOKEN_PREFIX as CUSTOM_TOKEN_ID_PREFIX
import com.tangem.domain.tokens.models.CryptoCurrency.ID.Prefix.TOKEN_PREFIX as TOKEN_ID_PREFIX
import com.tangem.domain.tokens.models.CryptoCurrency.ID.Suffix.ContractAddress as CustomCurrencyIdSuffix
import com.tangem.domain.tokens.models.CryptoCurrency.ID.Suffix.RawID as CurrencyIdSuffix
private const val DEFAULT_TOKENS_ICONS_HOST = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins"
private const val TOKEN_ICON_SIZE = "large"
private const val TOKEN_ICON_EXT = "png"
private const val COIN_ID_PREFIX = "coin_"
private const val TOKEN_ID_PREFIX = "token_"
private const val CUSTOM_TOKEN_ID_PREFIX = "custom_token_"
private const val TOKEN_ID_DELIMITER = '#'
internal fun isCustomToken(tokenId: CryptoCurrency.ID): Boolean {
return tokenId.value.startsWith(CUSTOM_TOKEN_ID_PREFIX)
internal fun isCustomToken(tokenId: ID): Boolean {
return tokenId.rawCurrencyId == null
}
internal fun getDerivationPath(blockchain: Blockchain, card: CardDTO): String? {
@ -41,24 +41,14 @@ internal fun getNetworkId(blockchain: Blockchain): Network.ID {
return Network.ID(value)
}
internal fun getCoinId(blockchain: Blockchain): CryptoCurrency.ID {
internal fun getCoinId(blockchain: Blockchain): ID {
return getTokenOrCoinId(blockchain, token = null)
}
internal fun getTokenId(blockchain: Blockchain, token: SdkToken): CryptoCurrency.ID {
internal fun getTokenId(blockchain: Blockchain, token: SdkToken): ID {
return getTokenOrCoinId(blockchain, token)
}
internal fun getTokenIdString(currencyId: CryptoCurrency.ID): String? {
val idValue = currencyId.value
return if (idValue.startsWith(CUSTOM_TOKEN_ID_PREFIX)) {
null
} else {
idValue.substringAfter(TOKEN_ID_DELIMITER)
}
}
internal fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? {
val tokenId = token.id
@ -79,22 +69,15 @@ internal fun getCoinIconUrl(blockchain: Blockchain): String? {
return coinId?.let(::getTokenIconUrlFromDefaultHost)
}
private fun getTokenOrCoinId(blockchain: Blockchain, token: SdkToken?): CryptoCurrency.ID {
private fun getTokenOrCoinId(blockchain: Blockchain, token: SdkToken?): ID {
val sdkTokenId = token?.id
val (prefix, suffix) = when {
token == null -> COIN_ID_PREFIX to blockchain.toCoinId()
sdkTokenId == null -> CUSTOM_TOKEN_ID_PREFIX to token.contractAddress
else -> TOKEN_ID_PREFIX to sdkTokenId
token == null -> COIN_ID_PREFIX to CurrencyIdSuffix(rawId = blockchain.toCoinId())
sdkTokenId == null -> CUSTOM_TOKEN_ID_PREFIX to CustomCurrencyIdSuffix(contractAddress = token.contractAddress)
else -> TOKEN_ID_PREFIX to CurrencyIdSuffix(rawId = sdkTokenId)
}
val value = buildString {
append(prefix)
append(blockchain.id)
append(TOKEN_ID_DELIMITER)
append(suffix.lowercase())
}
return CryptoCurrency.ID(value)
return ID(prefix, getNetworkId(blockchain), suffix)
}
private fun getTokenIconUrlFromDefaultHost(tokenId: String): String {

View file

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

View file

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

View file

@ -65,15 +65,68 @@ sealed class CryptoCurrency {
}
/**
* Value class for uniquely identifying a cryptocurrency.
* Represents a unique identifier for a cryptocurrency, constructed from various components.
*
* @property value The unique identifier value.
* The ID is designed to ensure that different cryptocurrencies, whether they are standard tokens, custom tokens or
* standard coins, can be distinctly identified within a system.
*
* @property value Constructed unique identifier value, made up of prefix, network ID, and suffix.
* @property rawCurrencyId Represents not unique currency ID from the blockchain network. `null` if
* its ID of the custom token.
*/
@JvmInline
value class ID(val value: String) {
data class ID(
private val prefix: Prefix,
private val networkId: Network.ID,
private val suffix: Suffix,
) {
init {
require(value.isNotBlank()) { "Crypto currency ID must not be blank" }
val value: String = buildString {
append(prefix.value)
append(networkId.value)
append(DELIMITER)
append(suffix.value)
}
val rawCurrencyId: String? = (suffix as? Suffix.RawID)?.rawId
/**
* Represents the different types of prefixes that can be associated with a cryptocurrency ID.
* These prefixes can help in quickly categorizing the type of cryptocurrency.
*/
enum class Prefix(val value: String) {
/** Prefix for standard coins. */
COIN_PREFIX(value = "coin_"),
/** Prefix for standard tokens. */
TOKEN_PREFIX(value = "token_"),
/** Prefix for custom tokens. */
CUSTOM_TOKEN_PREFIX(value = "custom_"),
}
/**
* Represents the suffix part of the cryptocurrency ID.
*
* The suffix can either be a raw ID or a contract address.
*/
sealed class Suffix {
/** The value of the suffix, which could be either a raw ID or a contract address. */
abstract val value: String
/** Represents a raw ID suffix. */
data class RawID(val rawId: String) : Suffix() {
override val value: String = rawId
}
/** Represents a contract address suffix. */
data class ContractAddress(val contractAddress: String) : Suffix() {
override val value: String = contractAddress
}
}
private companion object {
const val DELIMITER = '#'
}
}

View file

@ -5,12 +5,12 @@ import java.math.BigDecimal
/**
* Represents a financial quote for a specific cryptocurrency, including its fiat exchange rate and price change.
*
* @property currencyId The unique identifier of the cryptocurrency for which the quote is provided.
* @property rawCurrencyId The unique identifier of the token for which the quote is provided.
* @property fiatRate The current fiat exchange rate for the cryptocurrency.
* @property priceChange The price change for the cryptocurrency.
*/
data class Quote(
val currencyId: CryptoCurrency.ID,
val rawCurrencyId: String,
val fiatRate: BigDecimal,
val priceChange: BigDecimal,
)

View file

@ -79,7 +79,7 @@ internal class CurrenciesStatusesOperations(
val quoteFlow = getQuotes(nonEmptySetOf(currency.id))
.map { maybeQuotes ->
maybeQuotes.map { quotes ->
quotes.singleOrNull { it.currencyId == currency.id }
quotes.singleOrNull { it.rawCurrencyId == currency.id.rawCurrencyId }
}
}
@ -102,11 +102,11 @@ internal class CurrenciesStatusesOperations(
quotes: Set<Quote>,
networkStatuses: Set<NetworkStatus>,
): List<CryptoCurrencyStatus> {
return currencies.map { token ->
val quote = quotes.firstOrNull { it.currencyId == token.id }
val networkStatus = networkStatuses.firstOrNull { it.networkId == token.networkId }
return currencies.map { currency ->
val quote = quotes.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId }
val networkStatus = networkStatuses.firstOrNull { it.networkId == currency.networkId }
createStatus(token, quote, networkStatus)
createStatus(currency, quote, networkStatus)
}
}

View file

@ -8,61 +8,61 @@ import java.math.BigDecimal
internal object MockQuotes {
val quote1 = Quote(
currencyId = MockTokens.token1.id,
rawCurrencyId = MockTokens.token1.id.rawCurrencyId!!,
fiatRate = BigDecimal("1.23"),
priceChange = BigDecimal("0.01"),
)
val quote2 = Quote(
currencyId = MockTokens.token2.id,
rawCurrencyId = MockTokens.token2.id.rawCurrencyId!!,
fiatRate = BigDecimal("2.34"),
priceChange = BigDecimal("-0.02"),
)
val quote3 = Quote(
currencyId = MockTokens.token3.id,
rawCurrencyId = MockTokens.token3.id.rawCurrencyId!!,
fiatRate = BigDecimal("3.45"),
priceChange = BigDecimal("0.03"),
)
val quote4 = Quote(
currencyId = MockTokens.token4.id,
rawCurrencyId = MockTokens.token4.id.rawCurrencyId!!,
fiatRate = BigDecimal("4.56"),
priceChange = BigDecimal("-0.04"),
)
val quote5 = Quote(
currencyId = MockTokens.token5.id,
rawCurrencyId = MockTokens.token5.id.rawCurrencyId!!,
fiatRate = BigDecimal("5.67"),
priceChange = BigDecimal("0.05"),
)
val quote6 = Quote(
currencyId = MockTokens.token6.id,
rawCurrencyId = MockTokens.token6.id.rawCurrencyId!!,
fiatRate = BigDecimal("6.78"),
priceChange = BigDecimal("-0.06"),
)
val quote7 = Quote(
currencyId = MockTokens.token7.id,
rawCurrencyId = MockTokens.token7.id.rawCurrencyId!!,
fiatRate = BigDecimal("7.89"),
priceChange = BigDecimal("0.07"),
)
val quote8 = Quote(
currencyId = MockTokens.token8.id,
rawCurrencyId = MockTokens.token8.id.rawCurrencyId!!,
fiatRate = BigDecimal("8.90"),
priceChange = BigDecimal("-0.08"),
)
val quote9 = Quote(
currencyId = MockTokens.token9.id,
rawCurrencyId = MockTokens.token9.id.rawCurrencyId!!,
fiatRate = BigDecimal("9.01"),
priceChange = BigDecimal("0.09"),
)
val quote10 = Quote(
currencyId = MockTokens.token10.id,
rawCurrencyId = MockTokens.token10.id.rawCurrencyId!!,
fiatRate = BigDecimal("10.12"),
priceChange = BigDecimal("-0.10"),
)

View file

@ -1,12 +1,13 @@
package com.tangem.domain.tokens.mock
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.CryptoCurrency.ID
internal object MockTokens {
val token1
get() = CryptoCurrency.Coin(
id = CryptoCurrency.ID("token1"),
id = ID(ID.Prefix.COIN_PREFIX, MockNetworks.network1.id, ID.Suffix.RawID("token1")),
networkId = MockNetworks.network1.id,
name = "Token 1",
symbol = "T1",
@ -16,7 +17,7 @@ internal object MockTokens {
)
val token2
get() = CryptoCurrency.Token(
id = CryptoCurrency.ID("token2"),
id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network1.id, ID.Suffix.RawID("token2")),
networkId = MockNetworks.network1.id,
name = "Token 2",
symbol = "T2",
@ -28,7 +29,7 @@ internal object MockTokens {
)
val token3
get() = CryptoCurrency.Token(
id = CryptoCurrency.ID("token3"),
id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network1.id, ID.Suffix.RawID("token3")),
networkId = MockNetworks.network1.id,
name = "Token 3",
symbol = "T3",
@ -40,7 +41,7 @@ internal object MockTokens {
)
val token4
get() = CryptoCurrency.Coin(
id = CryptoCurrency.ID("token4"),
id = ID(ID.Prefix.COIN_PREFIX, MockNetworks.network2.id, ID.Suffix.RawID("token4")),
networkId = MockNetworks.network2.id,
name = "Token 4",
symbol = "T4",
@ -50,7 +51,7 @@ internal object MockTokens {
)
val token5
get() = CryptoCurrency.Token(
id = CryptoCurrency.ID("token5"),
id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network2.id, ID.Suffix.RawID("token5")),
networkId = MockNetworks.network2.id,
name = "Token 5",
symbol = "T5",
@ -62,7 +63,7 @@ internal object MockTokens {
)
val token6
get() = CryptoCurrency.Token(
id = CryptoCurrency.ID("token6"),
id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network2.id, ID.Suffix.RawID("token6")),
networkId = MockNetworks.network2.id,
name = "Token 6",
symbol = "T6",
@ -74,7 +75,7 @@ internal object MockTokens {
)
val token7
get() = CryptoCurrency.Coin(
id = CryptoCurrency.ID("token7"),
id = ID(ID.Prefix.COIN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token7")),
networkId = MockNetworks.network3.id,
name = "Token 7",
symbol = "T7",
@ -84,7 +85,7 @@ internal object MockTokens {
)
val token8
get() = CryptoCurrency.Token(
id = CryptoCurrency.ID("token8"),
id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token8")),
networkId = MockNetworks.network3.id,
name = "Token 8",
symbol = "T8",
@ -96,7 +97,7 @@ internal object MockTokens {
)
val token9
get() = CryptoCurrency.Token(
id = CryptoCurrency.ID("token9"),
id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token9")),
networkId = MockNetworks.network3.id,
name = "Token 9",
symbol = "T9",
@ -108,7 +109,7 @@ internal object MockTokens {
)
val token10
get() = CryptoCurrency.Token(
id = CryptoCurrency.ID("token10"),
id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token10")),
networkId = MockNetworks.network3.id,
name = "Token 10",
symbol = "T10",

View file

@ -74,7 +74,7 @@ internal object MockTokensStates {
val networkStatus = MockNetworks.verifiedNetworksStatuses
.first { it.networkId == status.currency.networkId }
val amount = (networkStatus.value as NetworkStatus.Verified).amounts[status.currency.id]!!
val quote = MockQuotes.quotes.first { it.currencyId == status.currency.id }
val quote = MockQuotes.quotes.first { it.rawCurrencyId == status.currency.id.rawCurrencyId }
val fiatAmount = amount * quote.fiatRate
status.copy(