Updated on 2026-08-14

This commit is contained in:
Tangem 2023-08-18 12:48:55 +03:00
parent f089d31106
commit eda6a624d0
26 changed files with 161 additions and 277 deletions

View file

@ -51,10 +51,9 @@ internal object TokensDomainModule {
@Provides
@ViewModelScoped
fun provideToggleTokenListGroupingUseCase(
networksRepository: NetworksRepository,
dispatchers: CoroutineDispatcherProvider,
): ToggleTokenListGroupingUseCase {
return ToggleTokenListGroupingUseCase(networksRepository, dispatchers)
return ToggleTokenListGroupingUseCase(dispatchers)
}
@Provides

View file

@ -7,7 +7,7 @@ import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.store
import com.tangem.utils.converter.Converter
class CryptoCurrencyConverter : Converter<Currency, CryptoCurrency> {
internal class CryptoCurrencyConverter : Converter<Currency, CryptoCurrency> {
private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() }

View file

@ -88,7 +88,7 @@ internal class DefaultNetworksRepository(
private suspend fun fetchNetworkStatus(userWalletId: UserWalletId, networkId: Network.ID) {
val currencies = getCurrencies(userWalletId)
.asSequence()
.filter { it.networkId == networkId }
.filter { it.network.id == networkId }
val result = walletManagersFacade.update(
userWalletId = userWalletId,

View file

@ -1,11 +1,12 @@
package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token as SdkToken
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.tokens.models.CryptoCurrency
import timber.log.Timber
import com.tangem.blockchain.common.Token as SdkToken
// FIXME: Make internal
class CryptoCurrencyFactory {
fun createToken(
@ -19,9 +20,10 @@ class CryptoCurrencyFactory {
}
val id = getTokenId(blockchain, sdkToken)
return CryptoCurrency.Token(
id = id,
networkId = getNetworkId(blockchain),
network = getNetwork(blockchain) ?: return null,
name = sdkToken.name,
symbol = sdkToken.symbol,
iconUrl = getTokenIconUrl(blockchain, sdkToken),
@ -29,8 +31,6 @@ class CryptoCurrencyFactory {
isCustom = isCustomToken(id),
contractAddress = sdkToken.contractAddress,
derivationPath = getDerivationPath(blockchain, derivationStyleProvider),
blockchainName = blockchain.fullName,
standardType = getTokenStandardType(blockchain, sdkToken),
)
}
@ -42,7 +42,7 @@ class CryptoCurrencyFactory {
return CryptoCurrency.Coin(
id = getCoinId(blockchain),
networkId = getNetworkId(blockchain),
network = getNetwork(blockchain) ?: return null,
name = blockchain.fullName,
symbol = blockchain.currency,
iconUrl = getCoinIconUrl(blockchain),

View file

@ -3,22 +3,13 @@ package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.tokens.models.Network
import com.tangem.utils.converter.Converter
import timber.log.Timber
internal class NetworkConverter : Converter<Network.ID, Network?> {
override fun convert(value: Network.ID): Network? {
val blockchain = Blockchain.fromId(value.value)
if (blockchain == Blockchain.Unknown) {
Timber.e("Unable to convert Unknown blockchain to the domain network model")
return null
}
return Network(
id = value,
name = blockchain.fullName,
)
return getNetwork(blockchain)
}
override fun convertList(input: Collection<Network.ID>): List<Network> {

View file

@ -0,0 +1,29 @@
package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.tokens.models.Network
import timber.log.Timber
internal fun getNetwork(blockchain: Blockchain): Network? {
if (blockchain == Blockchain.Unknown) {
Timber.e("Unable to convert Unknown blockchain to the domain network model")
return null
}
return Network(
id = Network.ID(blockchain.id),
name = blockchain.fullName,
isTestnet = blockchain.isTestnet(),
standardType = getNetworkStandardType(blockchain),
)
}
private fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType {
return when (blockchain) {
Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.StandardType.ERC20
Blockchain.BSC, Blockchain.BSCTestnet -> Network.StandardType.BEP20
Blockchain.Binance, Blockchain.BinanceTestnet -> Network.StandardType.BEP2
Blockchain.Tron, Blockchain.TronTestnet -> Network.StandardType.TRC20
else -> Network.StandardType.Unspecified(blockchain.name)
}
}

View file

@ -59,10 +59,10 @@ internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) {
}
}
private fun createCoin(blockchain: Blockchain, responseToken: UserTokensResponse.Token): CryptoCurrency.Coin {
private fun createCoin(blockchain: Blockchain, responseToken: UserTokensResponse.Token): CryptoCurrency.Coin? {
return CryptoCurrency.Coin(
id = getCoinId(blockchain),
networkId = getNetworkId(blockchain),
network = getNetwork(blockchain) ?: return null,
name = responseToken.name,
symbol = responseToken.symbol,
decimals = responseToken.decimals,
@ -71,12 +71,12 @@ internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) {
)
}
private fun createToken(blockchain: Blockchain, sdkToken: Token, derivationPath: String?): CryptoCurrency.Token {
private fun createToken(blockchain: Blockchain, sdkToken: Token, derivationPath: String?): CryptoCurrency.Token? {
val id = getTokenId(blockchain, sdkToken)
return CryptoCurrency.Token(
id = id,
networkId = getNetworkId(blockchain),
network = getNetwork(blockchain) ?: return null,
name = sdkToken.name,
symbol = sdkToken.symbol,
decimals = sdkToken.decimals,
@ -84,8 +84,6 @@ internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) {
iconUrl = getTokenIconUrl(blockchain, sdkToken),
contractAddress = sdkToken.contractAddress,
isCustom = isCustomToken(id),
blockchainName = blockchain.fullName,
standardType = getTokenStandardType(blockchain, sdkToken),
)
}
}

View file

@ -5,7 +5,6 @@ import com.tangem.blockchain.common.IconsUtil
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.common.extensions.toNetworkId
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
@ -31,12 +30,6 @@ internal fun getBlockchain(networkId: Network.ID): Blockchain {
return Blockchain.fromId(networkId.value)
}
internal fun getNetworkId(blockchain: Blockchain): Network.ID {
val value = blockchain.id
return Network.ID(value)
}
internal fun getCoinId(blockchain: Blockchain): ID {
return getTokenOrCoinId(blockchain, token = null)
}
@ -45,16 +38,6 @@ internal fun getTokenId(blockchain: Blockchain, token: SdkToken): ID {
return getTokenOrCoinId(blockchain, token)
}
internal fun getTokenStandardType(blockchain: Blockchain, token: SdkToken): CryptoCurrency.StandardType {
return when (blockchain) {
Blockchain.Ethereum, Blockchain.EthereumTestnet -> CryptoCurrency.StandardType.ERC20
Blockchain.BSC, Blockchain.BSCTestnet -> CryptoCurrency.StandardType.BEP20
Blockchain.Binance, Blockchain.BinanceTestnet -> CryptoCurrency.StandardType.BEP2
Blockchain.Tron, Blockchain.TronTestnet -> CryptoCurrency.StandardType.TRC20
else -> CryptoCurrency.StandardType.Unspecified(token.name)
}
}
internal fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? {
val tokenId = token.id
@ -83,7 +66,7 @@ private fun getTokenOrCoinId(blockchain: Blockchain, token: SdkToken?): ID {
else -> TOKEN_ID_PREFIX to CurrencyIdSuffix(rawId = sdkTokenId)
}
return ID(prefix, getNetworkId(blockchain), suffix)
return ID(prefix, Network.ID(blockchain.id), suffix)
}
private fun getTokenIconUrlFromDefaultHost(tokenId: String): String {

View file

@ -27,7 +27,7 @@ internal class UserTokensResponseFactory {
}
private fun createResponseToken(currency: CryptoCurrency): UserTokensResponse.Token {
val blockchain = getBlockchain(currency.networkId)
val blockchain = getBlockchain(currency.network.id)
return UserTokensResponse.Token(
id = currency.id.rawCurrencyId,

View file

@ -6,7 +6,7 @@ import java.io.Serializable
* Represents a generic cryptocurrency.
*
* @property id Unique identifier for the cryptocurrency.
* @property networkId Identifier for the network to which the cryptocurrency belongs.
* @property network The network to which the cryptocurrency belongs.
* @property name Human-readable name of the cryptocurrency.
* @property symbol Symbol of the cryptocurrency.
* @property decimals Number of decimal places used by the cryptocurrency.
@ -14,11 +14,11 @@ import java.io.Serializable
* @property derivationPath Optional path used for key derivation. `null` if the wallet does not support the
* [HD Wallet](https://coinsutra.com/hd-wallets-deterministic-wallet/) feature.
*/
// TODO: [REDACTED_JIRA] delete serializable
// FIXME: Remove serialization [REDACTED_JIRA]
sealed class CryptoCurrency : Serializable {
abstract val id: ID
abstract val networkId: Network.ID
abstract val network: Network
abstract val name: String
abstract val symbol: String
abstract val decimals: Int
@ -30,7 +30,7 @@ sealed class CryptoCurrency : Serializable {
*/
data class Coin(
override val id: ID,
override val networkId: Network.ID,
override val network: Network,
override val name: String,
override val symbol: String,
override val decimals: Int,
@ -51,7 +51,7 @@ sealed class CryptoCurrency : Serializable {
*/
data class Token(
override val id: ID,
override val networkId: Network.ID,
override val network: Network,
override val name: String,
override val symbol: String,
override val decimals: Int,
@ -59,8 +59,6 @@ sealed class CryptoCurrency : Serializable {
override val derivationPath: String?,
val contractAddress: String,
val isCustom: Boolean,
val blockchainName: String, // TODO: Move this field to proper entity
val standardType: StandardType, // TODO: Move this field to proper entity
) : CryptoCurrency() {
init {
@ -79,6 +77,7 @@ sealed class CryptoCurrency : Serializable {
* @property rawCurrencyId Represents not unique currency ID from the blockchain network. `null` if
* its ID of the custom token.
*/
// FIXME: Remove serialization [REDACTED_JIRA]
data class ID(
private val prefix: Prefix,
private val networkId: Network.ID,
@ -114,6 +113,7 @@ sealed class CryptoCurrency : Serializable {
*
* The suffix can either be a raw ID or a contract address.
*/
// FIXME: Remove serialization [REDACTED_JIRA]
sealed class Suffix : Serializable {
/** The value of the suffix, which could be either a raw ID or a contract address. */
@ -135,26 +135,6 @@ sealed class CryptoCurrency : Serializable {
}
}
sealed class StandardType {
abstract val name: String
object ERC20 : StandardType() {
override val name: String = "ERC20"
}
object TRC20 : StandardType() {
override val name: String = "TRC20"
}
object BEP20 : StandardType() {
override val name: String = "BEP20"
}
object BEP2 : StandardType() {
override val name: String = "BEP2"
}
class Unspecified(val tokenName: String) : StandardType() {
override val name: String = tokenName
}
}
protected fun checkProperties() {
require(name.isNotBlank()) { "Crypto currency name must not be blank" }
require(symbol.isNotBlank()) { "Crypto currency symbol must not be blank" }

View file

@ -1,29 +1,80 @@
package com.tangem.domain.tokens.models
import java.io.Serializable
/**
* Represents a blockchain network, identified by a unique ID and a human-readable name.
* Represents a blockchain network, identified by a unique ID, a human-readable name, and its standard type.
*
* @property id The unique identifier of the network, encapsulated as an inline value class.
* This class encapsulates the primary details of a blockchain network, such as its ID, name,
* whether it operates as a test network, and the type of blockchain standard it conforms to
* (e.g., ERC20, BEP20).
*
* @property id The unique identifier of the network.
* @property name The human-readable name of the network, such as "Ethereum" or "Bitcoin".
*
* @throws IllegalArgumentException If the name or ID is blank.
* @property isTestnet Indicates whether the network is a test network or a main network.
* @property standardType The type of blockchain standard the network adheres to.
*/
data class Network(val id: ID, val name: String) {
// FIXME: Remove serialization [REDACTED_JIRA]
data class Network(
val id: ID,
val name: String,
val isTestnet: Boolean,
val standardType: StandardType,
) : Serializable {
init {
require(name.isNotBlank()) { "Network name must not be blank" }
}
/**
* Represents a unique identifier for a network.
* Represents a unique identifier for a blockchain network.
*
* @property value The string value of the network ID.
* @property value The string representation of the network ID.
*/
// FIXME: Remove serialization [REDACTED_JIRA]
@JvmInline
value class ID(val value: String) {
value class ID(val value: String) : Serializable {
init {
require(value.isNotBlank()) { "Network ID must not be blank" }
}
}
/**
* Represents the type of blockchain standard that a network adheres to.
*
* Blockchain networks often follow certain standards that dictate how tokens operate on them.
* These standards can define functionalities such as how transactions are processed,
* how tokens are minted or burned, and more.
*
* @property name The human-readable name of the standard type.
*/
sealed class StandardType {
abstract val name: String
/** Represents the ERC20 token standard, common on the Ethereum network. */
object ERC20 : StandardType() {
override val name: String = "ERC20"
}
/** Represents the TRC20 token standard, common on the TRON network. */
object TRC20 : StandardType() {
override val name: String = "TRC20"
}
/** Represents the BEP20 token standard, common on the Binance Smart Chain network. */
object BEP20 : StandardType() {
override val name: String = "BEP20"
}
/** Represents the BEP2 token standard, common on the Binance Chain network. */
object BEP2 : StandardType() {
override val name: String = "BEP2"
}
/** Represents a network that does not adhere to a predefined standard type. */
class Unspecified(val networkName: String) : StandardType() {
override val name: String = networkName
}
}
}

View file

@ -1,18 +1,18 @@
package com.tangem.domain.tokens
import arrow.core.Either
import arrow.core.raise.*
import arrow.core.raise.Raise
import arrow.core.raise.either
import arrow.core.raise.ensure
import arrow.core.raise.withError
import com.tangem.domain.tokens.error.TokenListSortingError
import com.tangem.domain.tokens.error.mapper.mapToTokenListSortingError
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.tokens.operations.TokenListSortingOperations
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
class ToggleTokenListGroupingUseCase(
private val networksRepository: NetworksRepository,
private val dispatchers: CoroutineDispatcherProvider,
) {
@ -34,14 +34,10 @@ class ToggleTokenListGroupingUseCase(
private fun Raise<TokenListSortingError>.groupTokens(tokenList: TokenList.Ungrouped): TokenList.GroupedByNetwork {
val sortingOperations = TokenListSortingOperations(tokenList)
val tokens = withError(TokenListSortingOperations.Error::mapToTokenListSortingError) {
sortingOperations.getTokens().bind()
}
val networks = getNetworks(tokens.map { it.currency.networkId }.toSet())
return TokenList.GroupedByNetwork(
groups = withError(TokenListSortingOperations.Error::mapToTokenListSortingError) {
sortingOperations.getGroupedTokens(networks).bind()
sortingOperations.getGroupedTokens().bind()
},
totalFiatBalance = tokenList.totalFiatBalance,
sortedBy = sortingOperations.getSortType(),
@ -61,11 +57,4 @@ class ToggleTokenListGroupingUseCase(
sortedBy = sortingOperations.getSortType(),
)
}
private fun Raise<TokenListSortingError>.getNetworks(networksIds: Set<Network.ID>): Set<Network> {
return catch(
block = { networksRepository.getNetworks(networksIds) },
catch = { raise(TokenListSortingError.DataError(it)) },
)
}
}

View file

@ -36,11 +36,10 @@ class ToggleTokenListSortingUseCase(
tokenList: TokenList.GroupedByNetwork,
): TokenList.GroupedByNetwork {
val operations = getSortingOperations(tokenList)
val networks = tokenList.groups.map { it.network }.toSet()
return tokenList.copy(
groups = withError(TokenListSortingOperations.Error::mapToTokenListSortingError) {
operations.getGroupedTokens(networks).bind()
operations.getGroupedTokens().bind()
},
sortedBy = operations.getSortType(),
)

View file

@ -20,7 +20,5 @@ internal fun TokenListOperations.Error.mapToTokenListError(): TokenListError {
is TokenListOperations.Error.DataError -> TokenListError.DataError(this.cause)
is TokenListOperations.Error.UnableToSortTokenList ->
TokenListError.UnableToSortTokenList(this.unsortedTokenList)
is TokenListOperations.Error.UnableToGroupTokenList ->
TokenListError.UnableToSortTokenList(this.ungroupedTokenList)
}
}

View file

@ -6,7 +6,6 @@ import com.tangem.domain.tokens.operations.TokenListSortingOperations
internal fun TokenListSortingOperations.Error.mapToTokenListSortingError(): TokenListSortingError {
return when (this) {
is TokenListSortingOperations.Error.EmptyTokens -> TokenListSortingError.TokenListIsEmpty
is TokenListSortingOperations.Error.EmptyNetworks,
is TokenListSortingOperations.Error.NetworkNotFound,
-> TokenListSortingError.UnableToSortTokenList
}

View file

@ -105,7 +105,7 @@ internal class CurrenciesStatusesOperations(
val statusFlow = getNetworksStatuses(networksIds)
.map { maybeStatuses ->
maybeStatuses.map { statuses ->
statuses.singleOrNull { it.networkId == currency.networkId }
statuses.singleOrNull { it.networkId == currency.network.id }
}
}
@ -129,7 +129,7 @@ internal class CurrenciesStatusesOperations(
currencies.map { currency ->
val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId }
val networkStatus = networksStatuses?.firstOrNull { it.networkId == currency.networkId }
val networkStatus = networksStatuses?.firstOrNull { it.networkId == currency.network.id }
createStatus(currency, quote, networkStatus, ignoreQuote = quotesRetrievingFailed)
}
@ -205,7 +205,7 @@ internal class CurrenciesStatusesOperations(
currencies: NonEmptyList<CryptoCurrency>,
): Pair<NonEmptySet<Network.ID>, NonEmptySet<CryptoCurrency.ID>> {
val currencyIdToNetworkId = currencies.associate { currency ->
currency.id to currency.networkId
currency.id to currency.network.id
}
val currenciesIds = currencyIdToNetworkId.keys.toNonEmptySetOrNull()
val networksIds = currencyIdToNetworkId.values.toNonEmptySetOrNull()

View file

@ -5,16 +5,13 @@ import arrow.core.raise.*
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.*
@Suppress("LongParameterList")
internal class TokenListOperations(
private val currenciesRepository: CurrenciesRepository,
private val networksRepository: NetworksRepository,
private val userWalletId: UserWalletId,
private val tokens: List<CryptoCurrencyStatus>,
) {
@ -25,7 +22,6 @@ internal class TokenListOperations(
useCase: GetTokenListUseCase,
) : this(
currenciesRepository = useCase.currenciesRepository,
networksRepository = useCase.networksRepository,
userWalletId = userWalletId,
tokens = tokens,
)
@ -70,37 +66,21 @@ internal class TokenListOperations(
sortByBalance = isSortedByBalance,
)
return createTokenList(currencies, sortingOperations, fiatBalance, isGrouped)
return createTokenList(sortingOperations, fiatBalance, isGrouped)
}
private fun Raise<Error>.createTokenList(
tokens: NonEmptyList<CryptoCurrencyStatus>,
sortingOperations: TokenListSortingOperations,
fiatBalance: TokenList.FiatBalance,
isGrouped: Boolean,
): TokenList {
return if (isGrouped) {
val networks = ensureNotNull(getNetworks(tokens).toNonEmptySetOrNull()) {
Error.UnableToGroupTokenList(
ungroupedTokenList = createUngroupedTokenList(sortingOperations, fiatBalance),
)
}
createGroupedTokenList(sortingOperations, fiatBalance, networks)
createGroupedTokenList(sortingOperations, fiatBalance)
} else {
createUngroupedTokenList(sortingOperations, fiatBalance)
}
}
private fun Raise<Error>.getNetworks(tokensNes: NonEmptyList<CryptoCurrencyStatus>): Set<Network> {
val networksIds = tokensNes.map { it.currency.networkId }.toNonEmptySet()
return catch(
block = { networksRepository.getNetworks(networksIds) },
catch = { raise(Error.DataError(it)) },
)
}
private fun Raise<Error>.createUngroupedTokenList(
sortingOperations: TokenListSortingOperations,
fiatBalance: TokenList.FiatBalance,
@ -118,7 +98,6 @@ internal class TokenListOperations(
private fun Raise<Error>.createGroupedTokenList(
sortingOperations: TokenListSortingOperations,
fiatBalance: TokenList.FiatBalance,
networks: NonEmptySet<Network>,
): TokenList.GroupedByNetwork = TokenList.GroupedByNetwork(
sortedBy = sortingOperations.getSortType(),
totalFiatBalance = fiatBalance,
@ -126,7 +105,7 @@ internal class TokenListOperations(
transform = { e ->
Error.fromTokenListOperations(e) { createUnsortedUngroupedTokenList(tokens, fiatBalance) }
},
block = { sortingOperations.getGroupedTokens(networks).bind() },
block = { sortingOperations.getGroupedTokens().bind() },
),
)
@ -159,8 +138,6 @@ internal class TokenListOperations(
data class UnableToSortTokenList(val unsortedTokenList: TokenList.Ungrouped) : Error()
data class UnableToGroupTokenList(val ungroupedTokenList: TokenList.Ungrouped) : Error()
data class DataError(val cause: Throwable) : Error()
internal companion object {
@ -169,7 +146,6 @@ internal class TokenListOperations(
e: TokenListSortingOperations.Error,
createUnsortedUngroupedTokenList: () -> TokenList.Ungrouped,
): Error = when (e) {
is TokenListSortingOperations.Error.EmptyNetworks,
is TokenListSortingOperations.Error.EmptyTokens,
is TokenListSortingOperations.Error.NetworkNotFound,
-> UnableToSortTokenList(

View file

@ -1,10 +1,12 @@
package com.tangem.domain.tokens.operations
import arrow.core.*
import arrow.core.Either
import arrow.core.NonEmptyList
import arrow.core.raise.Raise
import arrow.core.raise.either
import arrow.core.raise.ensure
import arrow.core.raise.ensureNotNull
import arrow.core.toNonEmptyListOrNull
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.NetworkGroup
import com.tangem.domain.tokens.model.TokenList
@ -31,16 +33,13 @@ internal class TokenListSortingOperations(
sortByBalance = sortByBalance,
)
fun getGroupedTokens(networks: Set<Network>): Either<Error, NonEmptyList<NetworkGroup>> = either {
fun getGroupedTokens(): Either<Error, NonEmptyList<NetworkGroup>> = either {
ensure(currencies.isNotEmpty()) { Error.EmptyTokens }
val networksNes = ensureNotNull(networks.toNonEmptySetOrNull()) {
Error.EmptyNetworks
}
if (sortByBalance) {
groupAndSortTokensByBalance(networksNes)
groupAndSortTokensByBalance()
} else {
groupTokens(networksNes)
groupTokens()
}
}
@ -54,14 +53,10 @@ internal class TokenListSortingOperations(
fun getSortType(): TokenList.SortType = if (sortByBalance) TokenList.SortType.BALANCE else TokenList.SortType.NONE
private fun Raise<Error>.groupTokens(networks: NonEmptySet<Network>): NonEmptyList<NetworkGroup> {
private fun Raise<Error>.groupTokens(): NonEmptyList<NetworkGroup> {
val groupedTokens = currencies
.groupBy { it.currency.networkId }
.map { (networkId, tokens) ->
val network = ensureNotNull(networks.firstOrNull { it.id == networkId }) {
Error.NetworkNotFound(networkId)
}
.groupBy { it.currency.network }
.map { (network, tokens) ->
NetworkGroup(
network = network,
currencies = ensureNotNull(tokens.toNonEmptyListOrNull()) { Error.EmptyTokens },
@ -72,8 +67,8 @@ internal class TokenListSortingOperations(
return ensureNotNull(groupedTokens) { Error.EmptyTokens }
}
private fun Raise<Error>.groupAndSortTokensByBalance(networks: NonEmptySet<Network>): NonEmptyList<NetworkGroup> {
val groupsWithSortedTokens = groupTokens(networks)
private fun Raise<Error>.groupAndSortTokensByBalance(): NonEmptyList<NetworkGroup> {
val groupsWithSortedTokens = groupTokens()
.map { group ->
val tokens = group.currencies as? NonEmptyList<CryptoCurrencyStatus>
?: error("Tokens can not be empty here")
@ -110,8 +105,6 @@ internal class TokenListSortingOperations(
object EmptyTokens : Error()
object EmptyNetworks : Error()
data class NetworkNotFound(val networkId: Network.ID) : Error()
}
}

View file

@ -12,7 +12,6 @@ import com.tangem.domain.tokens.mock.MockTokens
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.tokens.models.Quote
import com.tangem.domain.tokens.repository.MockCurrenciesRepository
import com.tangem.domain.tokens.repository.MockNetworksRepository
@ -109,23 +108,6 @@ internal class GetTokenListUseCaseTest {
assertEquals(expectedResult, result)
}
@Test
fun `when networks getting failed and list is groped then error should be received`() = runTest {
// Given
val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left()
val useCase = getUseCase(
networks = DataError.NetworkError.NoInternetConnection.left(),
isGrouped = flowOf(true.right()),
)
// When
val result = useCase(userWalletId).first()
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when networks statuses getting failed then error should be received`() = runTest {
// Given
@ -212,22 +194,6 @@ internal class GetTokenListUseCaseTest {
assertEquals(expectedResult, result)
}
@Test
fun `when list is grouped and networks getting failed then error should be received`() = runTest {
val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left()
val useCase = getUseCase(
networks = DataError.NetworkError.NoInternetConnection.left(),
isGrouped = flowOf(true.right()),
)
// When
val result = useCase(userWalletId).first()
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when list is sorted and ungrouped then correct token list should be received`() = runTest {
val expectedResult = listOf(
@ -285,27 +251,6 @@ internal class GetTokenListUseCaseTest {
assertEquals(expectedResult, result)
}
@Test
fun `when networks is empty and list is grouped then ungrouped list should be received`() = runTest {
val expectedResult = listOf(
TokenListError.UnableToSortTokenList(MockTokenLists.loadingUngroupedTokenList).left(),
TokenListError.UnableToSortTokenList(MockTokenLists.failedUngroupedTokenList).left(),
)
val useCase = getUseCase(
networks = emptySet<Network>().right(),
isGrouped = flowOf(true.right()),
)
// When
val result = useCase(userWalletId)
.take(count = 2)
.toList()
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when tokens flow is empty then error should be received`() = runTest {
val expectedResult = TokenListError.EmptyTokens.left()
@ -390,7 +335,6 @@ internal class GetTokenListUseCaseTest {
private fun getUseCase(
tokens: Flow<Either<DataError, List<CryptoCurrency>>> = flowOf(MockTokens.tokens.right()),
quotes: Flow<Either<DataError, Set<Quote>>> = flowOf(MockQuotes.quotes.right()),
networks: Either<DataError, Set<Network>> = MockNetworks.networks.right(),
statuses: Flow<Either<DataError, Set<NetworkStatus>>> = flowOf(MockNetworks.errorNetworksStatuses.right()),
isGrouped: Flow<Either<DataError, Boolean>> = flowOf(MockTokenLists.isGrouped.right()),
isSortedByBalance: Flow<Either<DataError, Boolean>> = flowOf(MockTokenLists.isSortedByBalance.right()),
@ -404,6 +348,6 @@ internal class GetTokenListUseCaseTest {
isSortedByBalance = isSortedByBalance,
),
quotesRepository = MockQuotesRepository(quotes),
networksRepository = MockNetworksRepository(networks, statuses),
networksRepository = MockNetworksRepository(MockNetworks.networks.right(), statuses),
)
}

View file

@ -1,17 +1,11 @@
package com.tangem.domain.tokens
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.core.error.DataError
import com.tangem.domain.tokens.error.TokenListSortingError
import com.tangem.domain.tokens.mock.MockNetworks
import com.tangem.domain.tokens.mock.MockTokenLists
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.tokens.repository.MockNetworksRepository
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -143,38 +137,7 @@ internal class ToggleTokenListGroupingTest {
assertEquals(expectedResult, result)
}
@Test
fun `when list is ungrouped but networks is empty then error should be received`() = runTest {
// Given
val expectedResult = TokenListSortingError.UnableToSortTokenList.left()
val useCase = getUseCase(networks = emptySet<Network>().right())
// When
val result = useCase(MockTokenLists.unsortedUngroupedTokenList)
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when list is ungrouped but networks getting failed then error should be received`() = runTest {
// Given
val error = DataError.NetworkError.NoInternetConnection
val expectedResult = TokenListSortingError.DataError(error).left()
val useCase = getUseCase(networks = error.left())
// When
val result = useCase(MockTokenLists.unsortedUngroupedTokenList)
// Then
assertEquals(expectedResult, result)
}
private fun getUseCase(networks: Either<DataError, Set<Network>> = MockNetworks.networks.right()) =
ToggleTokenListGroupingUseCase(
networksRepository = MockNetworksRepository(networks, statuses = flowOf()),
dispatchers = TestingCoroutineDispatcherProvider(),
)
private fun getUseCase() = ToggleTokenListGroupingUseCase(
dispatchers = TestingCoroutineDispatcherProvider(),
)
}

View file

@ -14,16 +14,22 @@ internal object MockNetworks {
val network1 = Network(
id = Network.ID("network1"),
name = "Network One",
isTestnet = false,
standardType = Network.StandardType.ERC20,
)
val network2 = Network(
id = Network.ID("network2"),
name = "Network Two",
isTestnet = false,
standardType = Network.StandardType.ERC20,
)
val network3 = Network(
id = Network.ID("network3"),
name = "Network Three",
isTestnet = false,
standardType = Network.StandardType.ERC20,
)
val networks = nonEmptySetOf(network1, network2, network3)

View file

@ -10,21 +10,21 @@ internal object MockNetworksGroups {
val networkGroup1 = NetworkGroup(
network = MockNetworks.network1,
currencies = MockTokensStates.failedTokenStates
.filter { it.currency.networkId == MockNetworks.network1.id }
.filter { it.currency.network.id == MockNetworks.network1.id }
.toNonEmptyListOrNull()!!,
)
val networkGroup2 = NetworkGroup(
network = MockNetworks.network2,
currencies = MockTokensStates.failedTokenStates
.filter { it.currency.networkId == MockNetworks.network2.id }
.filter { it.currency.network.id == MockNetworks.network2.id }
.toNonEmptyListOrNull()!!,
)
val networkGroup3 = NetworkGroup(
network = MockNetworks.network3,
currencies = MockTokensStates.failedTokenStates
.filter { it.currency.networkId == MockNetworks.network3.id }
.filter { it.currency.network.id == MockNetworks.network3.id }
.toNonEmptyListOrNull()!!,
)
@ -33,7 +33,7 @@ internal object MockNetworksGroups {
val loadedNetworksGroups = failedNetworksGroups.map { group ->
group.copy(
currencies = MockTokensStates.loadedTokensStates
.filter { it.currency.networkId == group.network.id }
.filter { it.currency.network.id == group.network.id }
.toNonEmptyListOrNull()!!,
)
}

View file

@ -8,7 +8,7 @@ internal object MockTokens {
val token1
get() = CryptoCurrency.Coin(
id = ID(ID.Prefix.COIN_PREFIX, MockNetworks.network1.id, ID.Suffix.RawID("token1")),
networkId = MockNetworks.network1.id,
network = MockNetworks.network1,
name = "Token 1",
symbol = "T1",
decimals = 8,
@ -18,7 +18,7 @@ internal object MockTokens {
val token2
get() = CryptoCurrency.Token(
id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network1.id, ID.Suffix.RawID("token2")),
networkId = MockNetworks.network1.id,
network = MockNetworks.network1,
name = "Token 2",
symbol = "T2",
isCustom = false,
@ -26,13 +26,11 @@ internal object MockTokens {
iconUrl = null,
contractAddress = "address",
derivationPath = null,
blockchainName = "Ethereum",
standardType = CryptoCurrency.StandardType.ERC20,
)
val token3
get() = CryptoCurrency.Token(
id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network1.id, ID.Suffix.RawID("token3")),
networkId = MockNetworks.network1.id,
network = MockNetworks.network1,
name = "Token 3",
symbol = "T3",
isCustom = false,
@ -40,13 +38,11 @@ internal object MockTokens {
iconUrl = null,
contractAddress = "address",
derivationPath = null,
blockchainName = "Ethereum",
standardType = CryptoCurrency.StandardType.ERC20,
)
val token4
get() = CryptoCurrency.Coin(
id = ID(ID.Prefix.COIN_PREFIX, MockNetworks.network2.id, ID.Suffix.RawID("token4")),
networkId = MockNetworks.network2.id,
network = MockNetworks.network2,
name = "Token 4",
symbol = "T4",
decimals = 8,
@ -56,7 +52,7 @@ internal object MockTokens {
val token5
get() = CryptoCurrency.Token(
id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network2.id, ID.Suffix.RawID("token5")),
networkId = MockNetworks.network2.id,
network = MockNetworks.network2,
name = "Token 5",
symbol = "T5",
isCustom = false,
@ -64,13 +60,11 @@ internal object MockTokens {
iconUrl = null,
contractAddress = "address",
derivationPath = null,
blockchainName = "Ethereum",
standardType = CryptoCurrency.StandardType.ERC20,
)
val token6
get() = CryptoCurrency.Token(
id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network2.id, ID.Suffix.RawID("token6")),
networkId = MockNetworks.network2.id,
network = MockNetworks.network2,
name = "Token 6",
symbol = "T6",
isCustom = false,
@ -78,13 +72,11 @@ internal object MockTokens {
iconUrl = null,
contractAddress = "address",
derivationPath = null,
blockchainName = "Ethereum",
standardType = CryptoCurrency.StandardType.ERC20,
)
val token7
get() = CryptoCurrency.Coin(
id = ID(ID.Prefix.COIN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token7")),
networkId = MockNetworks.network3.id,
network = MockNetworks.network3,
name = "Token 7",
symbol = "T7",
decimals = 8,
@ -94,7 +86,7 @@ internal object MockTokens {
val token8
get() = CryptoCurrency.Token(
id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token8")),
networkId = MockNetworks.network3.id,
network = MockNetworks.network3,
name = "Token 8",
symbol = "T8",
isCustom = false,
@ -102,13 +94,11 @@ internal object MockTokens {
iconUrl = null,
contractAddress = "address",
derivationPath = null,
blockchainName = "Ethereum",
standardType = CryptoCurrency.StandardType.ERC20,
)
val token9
get() = CryptoCurrency.Token(
id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token9")),
networkId = MockNetworks.network3.id,
network = MockNetworks.network3,
name = "Token 9",
symbol = "T9",
isCustom = false,
@ -116,13 +106,11 @@ internal object MockTokens {
iconUrl = null,
contractAddress = "address",
derivationPath = null,
blockchainName = "Ethereum",
standardType = CryptoCurrency.StandardType.ERC20,
)
val token10
get() = CryptoCurrency.Token(
id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token10")),
networkId = MockNetworks.network3.id,
network = MockNetworks.network3,
name = "Token 10",
symbol = "T10",
isCustom = false,
@ -130,8 +118,6 @@ internal object MockTokens {
iconUrl = null,
contractAddress = "address",
derivationPath = null,
blockchainName = "Ethereum",
standardType = CryptoCurrency.StandardType.ERC20,
)
val tokens = listOf(token1, token2, token3, token4, token5, token6, token7, token8, token9, token10)

View file

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

View file

@ -28,8 +28,8 @@ internal class TokenDetailsSkeletonStateConverter(
currency = when (value.cryptoCurrency) {
is CryptoCurrency.Coin -> TokenInfoBlockState.Currency.Native
is CryptoCurrency.Token -> TokenInfoBlockState.Currency.Token(
networkName = value.cryptoCurrency.standardType.name,
blockchainName = value.cryptoCurrency.blockchainName,
networkName = value.cryptoCurrency.network.standardType.name,
blockchainName = value.cryptoCurrency.network.name,
// TODO: [REDACTED_JIRA]
networkIcon = R.drawable.img_eth_22,
)

View file

@ -45,7 +45,7 @@ internal class CryptoCurrencyToDraggableItemConverter(
): DraggableItem.Token {
return DraggableItem.Token(
tokenItemState = createTokenItemState(currencyStatus, appCurrency),
groupId = getGroupHeaderId(currencyStatus.currency.networkId),
groupId = getGroupHeaderId(currencyStatus.currency.network.id),
)
}