Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-13 14:19:25 +03:00
parent 4b5adcd08a
commit 5ed381c263
56 changed files with 631 additions and 455 deletions

View file

@ -54,7 +54,6 @@ internal class TokensListMigration(
}
is Either.Right -> {
currentUserWallet = selectedWalletEither.value
val derivationStyle = currentUserWallet.scanResponse.derivationStyleProvider.getDerivationStyle()
when (val currenciesEither = getCurrenciesUseCase(userWalletId = selectedWalletEither.value.walletId)) {
is Either.Left -> {
@ -65,7 +64,7 @@ internal class TokensListMigration(
TokensListCryptoCurrencies(
coins = currenciesEither.value
.filterIsInstance<CryptoCurrency.Coin>()
.filterNot { it.isCustomCurrency(derivationStyle) }
.filterNot { it.isCustom }
.also { currentNewCoins = it }
.map { Blockchain.fromId(it.network.id.value) },
tokens = currenciesEither.value
@ -91,12 +90,6 @@ internal class TokensListMigration(
}
}
private fun CryptoCurrency.Coin.isCustomCurrency(derivationStyle: DerivationStyle?): Boolean {
if (derivationPath == null || derivationStyle == null) return false
return derivationPath != Blockchain.fromId(network.id.value).derivationPath(derivationStyle)?.rawPath
}
private fun getLegacyCryptoCurrencies(): TokensListCryptoCurrencies {
val wallets = store.state.walletState.walletsDataFromStores
val derivationStyle = store.state.globalState.scanResponse?.derivationStyleProvider?.getDerivationStyle()
@ -157,12 +150,14 @@ internal class TokensListMigration(
cryptoCurrencyFactory.createToken(
sdkToken = it.token,
blockchain = it.blockchain,
extraDerivationPath = null,
derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider,
)
},
changedCoins = changedBlockchainList.mapNotNull {
cryptoCurrencyFactory.createCoin(
blockchain = it,
extraDerivationPath = null,
derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider,
)
},

View file

@ -298,7 +298,7 @@ object TokensMiddleware {
val customTokensCandidates = currencyList
.filter { Blockchain.fromId(it.network.id.value).getSupportedCurves().contains(curve) }
.mapNotNull(CryptoCurrency::derivationPath)
.mapNotNull { it.network.derivationPath.value }
.map(::DerivationPath)
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct().toMutableList()
@ -306,7 +306,7 @@ object TokensMiddleware {
currencyList.find { it is CryptoCurrency.Coin && Blockchain.fromId(it.network.id.value) == Blockchain.Cardano }
?.let { currency ->
currency.derivationPath?.let {
currency.network.derivationPath.value?.let {
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
}
}

View file

@ -18,6 +18,7 @@ internal class CryptoCurrencyConverter : TwoWayConverter<Currency, CryptoCurrenc
is Currency.Blockchain -> requireNotNull(
cryptoCurrencyFactory.createCoin(
blockchain = value.blockchain,
extraDerivationPath = value.derivationPath,
derivationStyleProvider = requireNotNull(
store.state.globalState
.userWalletsListManager
@ -31,6 +32,7 @@ internal class CryptoCurrencyConverter : TwoWayConverter<Currency, CryptoCurrenc
cryptoCurrencyFactory.createToken(
sdkToken = value.token,
blockchain = value.blockchain,
extraDerivationPath = value.derivationPath,
derivationStyleProvider = requireNotNull(
store.state.globalState
.userWalletsListManager
@ -49,7 +51,7 @@ internal class CryptoCurrencyConverter : TwoWayConverter<Currency, CryptoCurrenc
return when (value) {
is CryptoCurrency.Coin -> Currency.Blockchain(
blockchain = blockchain,
derivationPath = value.derivationPath,
derivationPath = value.network.derivationPath.value,
)
is CryptoCurrency.Token -> Currency.Token(
token = Token(
@ -60,7 +62,7 @@ internal class CryptoCurrencyConverter : TwoWayConverter<Currency, CryptoCurrenc
id = value.id.value,
),
blockchain = blockchain,
derivationPath = value.derivationPath,
derivationPath = value.network.derivationPath.value,
)
}
}

View file

@ -11,7 +11,6 @@ import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.feature.swap.presentation.SwapFragment
@ -128,9 +127,7 @@ class TradeCryptoMiddleware {
.getOrCreateWalletManager(
userWallet = action.userWallet,
blockchain = blockchain,
derivationPath = blockchain.derivationPath(
style = action.userWallet.scanResponse.derivationStyleProvider.getDerivationStyle(),
),
derivationPath = currency.network.derivationPath.value,
)
if (walletManager !is EthereumWalletManager) {
@ -317,9 +314,7 @@ class TradeCryptoMiddleware {
.getOrCreateWalletManager(
userWallet = action.userWallet,
blockchain = blockchain,
derivationPath = blockchain.derivationPath(
style = action.userWallet.scanResponse.derivationStyleProvider.getDerivationStyle(),
),
derivationPath = currency.network.derivationPath.value,
)
if (walletManager == null) {
@ -360,9 +355,7 @@ class TradeCryptoMiddleware {
.getOrCreateWalletManager(
userWallet = action.userWallet,
blockchain = blockchain,
derivationPath = blockchain.derivationPath(
style = action.userWallet.scanResponse.derivationStyleProvider.getDerivationStyle(),
),
derivationPath = currency.network.derivationPath.value,
)
if (walletManager == null) {

View file

@ -3,6 +3,8 @@ package com.tangem.utils.extensions
/**
* Removes an element from the collection based on the provided predicate.
*
* !!!This function is not thread-safe!!!
*
* @param predicate The condition to remove an element.
* @return [Boolean] indicating whether an element was removed.
*/
@ -33,7 +35,8 @@ inline fun <T> MutableList<T>.replaceBy(item: T, predicate: (T) -> Boolean): Boo
/**
* Adds the specified element to the list or replaces an existing element.
* The predicate defines the condition to replace the existing element.
*
* !!!This function is not thread-safe!!!
*
* @param item The element to be added or replace the existing one.
* @param predicate The condition to replace an existing element.

View file

@ -0,0 +1,39 @@
package com.tangem.utils.extensions
/**
* Replaces an element in the set with the provided item based on the predicate.
*
* !!!This function is not thread-safe!!!
*
* @param item The element to replace the existing one.
* @param predicate The condition to replace an existing element.
* @return [Boolean] indicating whether an element was replaced.
*/
inline fun <T> MutableSet<T>.replaceBy(item: T, predicate: (T) -> Boolean): Boolean {
val foundItem = firstOrNull(predicate) ?: return false
remove(foundItem)
add(item)
return true
}
/**
* Adds the specified element to the set or replaces an existing element.
*
* !!!This function is not thread-safe!!!
*
* @param item The element to be added or replace the existing one.
* @param predicate The condition to replace an existing element.
* @return The modified [Set] after adding or replacing the element.
*/
inline fun <T> Set<T>.addOrReplace(item: T, predicate: (T) -> Boolean): Set<T> {
val mutableList = this.toMutableSet()
val isReplaced = mutableList.replaceBy(item, predicate)
if (!isReplaced) {
mutableList.add(item)
}
return mutableList
}

View file

@ -8,6 +8,7 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
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.common.extensions.toCoinId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.core.error.DataError
@ -35,8 +36,8 @@ internal class DefaultCurrenciesRepository(
) : CurrenciesRepository {
private val demoConfig = DemoConfig()
private val responseCurrenciesFactory = ResponseCurrenciesFactory(demoConfig)
private val cardCurrenciesFactory = CardCurrenciesFactory(demoConfig)
private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(demoConfig)
private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(demoConfig)
private val userTokensResponseFactory = UserTokensResponseFactory()
override suspend fun saveTokens(
@ -90,6 +91,7 @@ internal class DefaultCurrenciesRepository(
.mapNotNull {
CryptoCurrencyFactory().createCoin(
blockchain = getBlockchain(networkId = it.network.id),
extraDerivationPath = it.network.derivationPath.value,
derivationStyleProvider = getUserWallet(userWalletId).scanResponse.derivationStyleProvider,
)
}
@ -99,7 +101,7 @@ internal class DefaultCurrenciesRepository(
return any {
val blockchain = getBlockchain(networkId = token.network.id)
it.id == getCoinId(blockchain).rawCurrencyId
it.id == blockchain.toCoinId()
}
}
@ -173,10 +175,7 @@ internal class DefaultCurrenciesRepository(
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
}
return responseCurrenciesFactory.createCurrencies(
response = storedTokens,
card = userWallet.scanResponse.card,
)
return responseCurrenciesFactory.createCurrencies(storedTokens, userWallet.scanResponse)
}
override suspend fun getMultiCurrencyWalletCurrency(
@ -190,7 +189,7 @@ internal class DefaultCurrenciesRepository(
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
}
responseCurrenciesFactory.createCurrency(id, response, userWallet.scanResponse.card)
responseCurrenciesFactory.createCurrency(id, response, userWallet.scanResponse)
}
override suspend fun getNetworkCoin(userWalletId: UserWalletId, networkId: Network.ID): CryptoCurrency.Coin {
@ -206,10 +205,7 @@ internal class DefaultCurrenciesRepository(
val storedCoin = storedTokens.tokens.find { it.networkId == Blockchain.fromId(networkId.value).toNetworkId() }
?: error("Coin in this network $networkId not found")
val coin = responseCurrenciesFactory.createCurrency(
responseToken = storedCoin,
card = userWallet.scanResponse.card,
)
val coin = responseCurrenciesFactory.createCurrency(storedCoin, userWallet.scanResponse)
return coin as? CryptoCurrency.Coin ?: error("Unable to create currency")
}
@ -242,7 +238,7 @@ internal class DefaultCurrenciesRepository(
return userTokensStore.get(userWallet.walletId).map { storedTokens ->
responseCurrenciesFactory.createCurrencies(
response = storedTokens,
card = userWallet.scanResponse.card,
scanResponse = userWallet.scanResponse,
)
}
}
@ -288,10 +284,7 @@ internal class DefaultCurrenciesRepository(
if (NOT_FOUND_HTTP_CODE in errorMessage) {
val response = userTokensStore.getSyncOrNull(userWallet.walletId)
?: userTokensResponseFactory.createUserTokensResponse(
currencies = cardCurrenciesFactory.createDefaultCoinsForMultiCurrencyCard(
card = userWallet.scanResponse.card,
derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider,
),
currencies = cardCurrenciesFactory.createDefaultCoinsForMultiCurrencyCard(userWallet.scanResponse),
isGroupedByNetwork = false,
isSortedByBalance = false,
)

View file

@ -1,10 +1,9 @@
package com.tangem.data.tokens.repository
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.tokens.utils.CardCurrenciesFactory
import com.tangem.data.tokens.utils.NetworkConverter
import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory
import com.tangem.data.tokens.utils.NetworkStatusFactory
import com.tangem.data.tokens.utils.ResponseCurrenciesFactory
import com.tangem.data.tokens.utils.ResponseCryptoCurrenciesFactory
import com.tangem.datasource.local.token.UserTokensStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.demo.DemoConfig
@ -13,7 +12,6 @@ import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.addOrReplace
@ -29,25 +27,18 @@ internal class DefaultNetworksRepository(
) : NetworksRepository {
private val demoConfig by lazy { DemoConfig() }
private val networkConverter by lazy { NetworkConverter() }
private val cardCurrenciesFactory by lazy { CardCurrenciesFactory(demoConfig) }
private val responseCurrenciesFactory by lazy { ResponseCurrenciesFactory(demoConfig) }
private val cardCurrenciesFactory by lazy { CardCryptoCurrenciesFactory(demoConfig) }
private val responseCurrenciesFactory by lazy { ResponseCryptoCurrenciesFactory(demoConfig) }
private val networkStatusFactory by lazy { NetworkStatusFactory() }
private val networksStatuses: MutableStateFlow<List<NetworkStatus>> = MutableStateFlow(emptyList())
override fun getNetworks(networksIds: Set<Network.ID>): Set<Network> {
return networkConverter.convertSet(networksIds)
}
private val networksStatuses: MutableStateFlow<Set<NetworkStatus>> = MutableStateFlow(hashSetOf())
override fun getNetworkStatusesUpdates(
userWalletId: UserWalletId,
networks: Set<Network.ID>,
networks: Set<Network>,
): Flow<Set<NetworkStatus>> = channelFlow {
launch(dispatchers.io) {
networksStatuses.collect {
send(it.toSet())
}
networksStatuses.collect(::send)
}
launch(dispatchers.io) {
@ -57,7 +48,7 @@ internal class DefaultNetworksRepository(
override suspend fun getNetworkStatusesSync(
userWalletId: UserWalletId,
networks: Set<Network.ID>,
networks: Set<Network>,
refresh: Boolean,
): Set<NetworkStatus> = withContext(dispatchers.io) {
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh)
@ -66,14 +57,14 @@ internal class DefaultNetworksRepository(
private suspend fun fetchNetworksStatusesIfCacheExpired(
userWalletId: UserWalletId,
networks: Set<Network.ID>,
networks: Set<Network>,
refresh: Boolean,
) {
coroutineScope {
networks
.map { networkId ->
.map { network ->
async {
fetchNetworkStatusIfCacheExpired(userWalletId, networkId, refresh)
fetchNetworkStatusIfCacheExpired(userWalletId, network, refresh)
}
}
.awaitAll()
@ -82,67 +73,70 @@ internal class DefaultNetworksRepository(
private suspend fun fetchNetworkStatusIfCacheExpired(
userWalletId: UserWalletId,
networkId: Network.ID,
network: Network,
refresh: Boolean,
) {
cacheRegistry.invokeOnExpire(
key = getNetworksStatusesCacheKey(userWalletId, networkId),
key = getNetworksStatusesCacheKey(userWalletId, network),
skipCache = refresh,
block = { fetchNetworkStatus(userWalletId, networkId) },
block = { fetchNetworkStatus(userWalletId, network) },
)
}
private suspend fun fetchNetworkStatus(userWalletId: UserWalletId, networkId: Network.ID) {
val currencies = getCurrencies(userWalletId)
.asSequence()
.filter { it.network.id == networkId }
private suspend fun fetchNetworkStatus(userWalletId: UserWalletId, network: Network) {
val currencies = getCurrencies(userWalletId, network)
val result = walletManagersFacade.update(
userWalletId = userWalletId,
networkId = networkId,
network = network,
extraTokens = currencies.filterIsInstance<CryptoCurrency.Token>().toSet(),
)
// Invalidate cache key if wallet manager update failed
when (result) {
is UpdateWalletManagerResult.Verified,
is UpdateWalletManagerResult.NoAccount,
-> Unit
is UpdateWalletManagerResult.Unreachable,
is UpdateWalletManagerResult.MissedDerivation,
-> cacheRegistry.invalidate(getNetworksStatusesCacheKey(userWalletId, networkId))
}
val networkStatus = networkStatusFactory.createNetworkStatus(
networkId = networkId,
network = network,
result = result,
currencies = currencies.toSet(),
)
networksStatuses.update { statuses ->
statuses.addOrReplace(networkStatus) { it.networkId == networkStatus.networkId }
statuses.addOrReplace(networkStatus) { it.network == networkStatus.network }
}
invalidateCacheKeyIfNeeded(userWalletId, networkStatus)
}
private suspend fun getCurrencies(userWalletId: UserWalletId): List<CryptoCurrency> {
private suspend fun getCurrencies(userWalletId: UserWalletId, network: Network): Sequence<CryptoCurrency> {
val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"Unable to find user wallet with provided ID: $userWalletId"
}
return if (userWallet.isMultiCurrency) {
val currencies = if (userWallet.isMultiCurrency) {
val response = requireNotNull(userTokensStore.getSyncOrNull(userWalletId)) {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
}
responseCurrenciesFactory.createCurrencies(response, userWallet.scanResponse.card)
responseCurrenciesFactory.createCurrencies(response, userWallet.scanResponse).asSequence()
} else {
val currency = cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse)
listOf(currency)
sequenceOf(currency)
}
return currencies.filter { it.network == network }
}
private suspend fun invalidateCacheKeyIfNeeded(userWalletId: UserWalletId, networkStatus: NetworkStatus) {
when (networkStatus.value) {
is NetworkStatus.Verified,
is NetworkStatus.NoAccount,
-> Unit
is NetworkStatus.Unreachable,
is NetworkStatus.MissedDerivation,
-> cacheRegistry.invalidate(getNetworksStatusesCacheKey(userWalletId, networkStatus.network))
}
}
private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId, nerworkId: Network.ID): String {
return "network_status_${userWalletId}_${nerworkId.value}"
private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId, network: Network): String {
return "network_status_${userWalletId}_${network.id}_${network.derivationPath.value}"
}
}

View file

@ -0,0 +1,61 @@
package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.models.CryptoCurrency
internal class CardCryptoCurrenciesFactory(private val demoConfig: DemoConfig) {
private val cryptoCurrencyFactory = CryptoCurrencyFactory()
fun createDefaultCoinsForMultiCurrencyCard(scanResponse: ScanResponse): List<CryptoCurrency.Coin> {
val cardDerivationStyleProvider = scanResponse.derivationStyleProvider
val card = scanResponse.card
var blockchains = if (demoConfig.isDemoCardId(card.cardId)) {
demoConfig.demoBlockchains
} else {
listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
}
if (card.isTestCard) {
blockchains = blockchains.mapNotNull { it.getTestnetVersion() }
}
return blockchains.mapNotNull {
cryptoCurrencyFactory.createCoin(
blockchain = it,
extraDerivationPath = null,
derivationStyleProvider = cardDerivationStyleProvider,
)
}
}
fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency {
val cardDerivationStyleProvider = scanResponse.derivationStyleProvider
val resolver = scanResponse.cardTypesResolver
val blockchain = resolver.getBlockchain()
val coin = cryptoCurrencyFactory.createCoin(
blockchain = blockchain,
extraDerivationPath = null,
derivationStyleProvider = cardDerivationStyleProvider,
)
requireNotNull(coin) { "Coin for the single currency card cannot be null" }
val primaryToken = resolver.getPrimaryToken()?.let { token ->
cryptoCurrencyFactory.createToken(
sdkToken = token,
blockchain = blockchain,
extraDerivationPath = null,
derivationStyleProvider = cardDerivationStyleProvider,
)
}
return primaryToken ?: coin
}
}

View file

@ -1,48 +0,0 @@
package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.models.CryptoCurrency
internal class CardCurrenciesFactory(private val demoConfig: DemoConfig) {
private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() }
fun createDefaultCoinsForMultiCurrencyCard(
card: CardDTO,
derivationStyleProvider: DerivationStyleProvider,
): List<CryptoCurrency.Coin> {
var blockchains = if (demoConfig.isDemoCardId(card.cardId)) {
demoConfig.demoBlockchains
} else {
listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
}
if (card.isTestCard) {
blockchains = blockchains.mapNotNull { it.getTestnetVersion() }
}
return blockchains.mapNotNull { cryptoCurrencyFactory.createCoin(it, derivationStyleProvider) }
}
fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency {
val derivationStyleProvider = scanResponse.derivationStyleProvider
val resolver = scanResponse.cardTypesResolver
val blockchain = resolver.getBlockchain()
val coin = requireNotNull(cryptoCurrencyFactory.createCoin(blockchain, derivationStyleProvider)) {
"Coin for the single currency card cannot be null"
}
val primaryToken = resolver.getPrimaryToken()?.let { token ->
cryptoCurrencyFactory.createToken(token, blockchain, derivationStyleProvider)
}
return primaryToken ?: coin
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.tokens.models.CryptoCurrency
import timber.log.Timber
import com.tangem.blockchain.common.Token as SdkToken
@ -12,6 +13,7 @@ class CryptoCurrencyFactory {
fun createToken(
sdkToken: SdkToken,
blockchain: Blockchain,
extraDerivationPath: String?,
derivationStyleProvider: DerivationStyleProvider,
): CryptoCurrency.Token? {
if (blockchain == Blockchain.Unknown) {
@ -19,35 +21,40 @@ class CryptoCurrencyFactory {
return null
}
val id = getTokenId(blockchain, sdkToken)
val network = getNetwork(blockchain, extraDerivationPath, derivationStyleProvider) ?: return null
val id = getTokenId(network, sdkToken)
return CryptoCurrency.Token(
id = id,
network = getNetwork(blockchain) ?: return null,
network = network,
name = sdkToken.name,
symbol = sdkToken.symbol,
iconUrl = getTokenIconUrl(blockchain, sdkToken),
decimals = sdkToken.decimals,
isCustom = isCustomToken(id),
isCustom = isCustomToken(id, network),
contractAddress = sdkToken.contractAddress,
derivationPath = getDerivationPath(blockchain, derivationStyleProvider),
)
}
fun createCoin(blockchain: Blockchain, derivationStyleProvider: DerivationStyleProvider): CryptoCurrency.Coin? {
fun createCoin(
blockchain: Blockchain,
extraDerivationPath: String?,
derivationStyleProvider: DerivationStyleProvider,
): CryptoCurrency.Coin? {
if (blockchain == Blockchain.Unknown) {
Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain")
return null
}
val network = getNetwork(blockchain, extraDerivationPath, derivationStyleProvider) ?: return null
return CryptoCurrency.Coin(
id = getCoinId(blockchain),
network = getNetwork(blockchain) ?: return null,
id = getCoinId(network, blockchain.toCoinId()),
network = network,
name = blockchain.fullName,
symbol = blockchain.currency,
iconUrl = getCoinIconUrl(blockchain),
decimals = blockchain.decimals(),
derivationPath = getDerivationPath(blockchain, derivationStyleProvider),
isCustom = isCustomCoin(network),
)
}
}

View file

@ -1,22 +0,0 @@
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
internal class NetworkConverter : Converter<Network.ID, Network?> {
override fun convert(value: Network.ID): Network? {
val blockchain = Blockchain.fromId(value.value)
return getNetwork(blockchain)
}
override fun convertList(input: Collection<Network.ID>): List<Network> {
return input.mapNotNull(::convert)
}
override fun convertSet(input: Collection<Network.ID>): Set<Network> {
return input.mapNotNullTo(hashSetOf(), ::convert)
}
}

View file

@ -1,10 +1,19 @@
package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.tokens.models.Network
import timber.log.Timber
internal fun getNetwork(blockchain: Blockchain): Network? {
internal fun getBlockchain(networkId: Network.ID): Blockchain {
return Blockchain.fromId(networkId.value)
}
internal fun getNetwork(
blockchain: Blockchain,
extraDerivationPath: String?,
derivationStyleProvider: DerivationStyleProvider,
): Network? {
if (blockchain == Blockchain.Unknown) {
Timber.e("Unable to convert Unknown blockchain to the domain network model")
return null
@ -14,10 +23,26 @@ internal fun getNetwork(blockchain: Blockchain): Network? {
id = Network.ID(blockchain.id),
name = blockchain.fullName,
isTestnet = blockchain.isTestnet(),
derivationPath = getDerivationPath(blockchain, extraDerivationPath, derivationStyleProvider),
standardType = getNetworkStandardType(blockchain),
)
}
private fun getDerivationPath(
blockchain: Blockchain,
extraDerivationPath: String?,
derivationStyleProvider: DerivationStyleProvider,
): Network.DerivationPath {
val cardDerivationPath = getCardDerivationPath(blockchain, derivationStyleProvider)
return when {
cardDerivationPath.isNullOrBlank() -> Network.DerivationPath.None
extraDerivationPath == cardDerivationPath -> Network.DerivationPath.Card(extraDerivationPath)
!extraDerivationPath.isNullOrBlank() -> Network.DerivationPath.Custom(extraDerivationPath)
else -> Network.DerivationPath.None
}
}
private fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType {
return when (blockchain) {
Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.StandardType.ERC20
@ -26,4 +51,8 @@ private fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType
Blockchain.Tron, Blockchain.TronTestnet -> Network.StandardType.TRC20
else -> Network.StandardType.Unspecified(blockchain.name)
}
}
private fun getCardDerivationPath(blockchain: Blockchain, derivationStyleProvider: DerivationStyleProvider): String? {
return blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())?.rawPath
}

View file

@ -14,12 +14,12 @@ import java.math.BigDecimal
internal class NetworkStatusFactory {
fun createNetworkStatus(
networkId: Network.ID,
network: Network,
result: UpdateWalletManagerResult,
currencies: Set<CryptoCurrency>,
): NetworkStatus {
return NetworkStatus(
networkId = networkId,
network = network,
value = when (result) {
is UpdateWalletManagerResult.MissedDerivation -> NetworkStatus.MissedDerivation
is UpdateWalletManagerResult.Unreachable -> NetworkStatus.Unreachable

View file

@ -3,47 +3,57 @@ package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.models.CryptoCurrency
import timber.log.Timber
import com.tangem.blockchain.common.Token as SdkToken
internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) {
internal class ResponseCryptoCurrenciesFactory(private val demoConfig: DemoConfig) {
fun createCurrency(currencyId: CryptoCurrency.ID, response: UserTokensResponse, card: CardDTO): CryptoCurrency {
fun createCurrency(
currencyId: CryptoCurrency.ID,
response: UserTokensResponse,
scanResponse: ScanResponse,
): CryptoCurrency {
val responseTokenId = currencyId.rawCurrencyId
val token = requireNotNull(response.tokens.firstOrNull { it.id == responseTokenId }) {
"Unable find a token with provided ID: $responseTokenId"
}
return requireNotNull(createCurrency(token, card)) {
return requireNotNull(createCurrency(token, scanResponse)) {
"Unable to create a currency with provided ID: $currencyId"
}
}
fun createCurrencies(response: UserTokensResponse, card: CardDTO): List<CryptoCurrency> {
return response.tokens.mapNotNull { createCurrency(it, card) }
fun createCurrencies(response: UserTokensResponse, scanResponse: ScanResponse): List<CryptoCurrency> {
return response.tokens.mapNotNull { createCurrency(it, scanResponse) }
}
fun createCurrency(responseToken: UserTokensResponse.Token, card: CardDTO): CryptoCurrency? {
fun createCurrency(responseToken: UserTokensResponse.Token, scanResponse: ScanResponse): CryptoCurrency? {
var blockchain = Blockchain.fromNetworkId(responseToken.networkId)
if (blockchain == null || blockchain == Blockchain.Unknown) {
Timber.e("Unable to find a blockchain with the network ID: ${responseToken.networkId}")
return null
}
val cardDerivationStyleProvider = scanResponse.derivationStyleProvider
val card = scanResponse.card
if (demoConfig.isDemoCardId(card.cardId)) {
blockchain = blockchain.getTestnetVersion() ?: blockchain
}
val sdkToken = createSdkToken(responseToken)
return if (sdkToken == null) {
createCoin(blockchain, responseToken)
createCoin(blockchain, responseToken, cardDerivationStyleProvider)
} else {
createToken(blockchain, sdkToken, responseToken.derivationPath)
createToken(blockchain, sdkToken, responseToken.derivationPath, cardDerivationStyleProvider)
}
}
@ -59,31 +69,43 @@ 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,
derivationStyleProvider: DerivationStyleProvider,
): CryptoCurrency.Coin? {
val network = getNetwork(blockchain, responseToken.derivationPath, derivationStyleProvider) ?: return null
return CryptoCurrency.Coin(
id = getCoinId(blockchain),
network = getNetwork(blockchain) ?: return null,
id = getCoinId(network, blockchain.toCoinId()),
network = network,
name = responseToken.name,
symbol = responseToken.symbol,
decimals = responseToken.decimals,
derivationPath = responseToken.derivationPath,
iconUrl = getCoinIconUrl(blockchain),
isCustom = isCustomCoin(network),
)
}
private fun createToken(blockchain: Blockchain, sdkToken: Token, derivationPath: String?): CryptoCurrency.Token? {
val id = getTokenId(blockchain, sdkToken)
private fun createToken(
blockchain: Blockchain,
sdkToken: Token,
responseDerivationPath: String?,
derivationStyleProvider: DerivationStyleProvider,
): CryptoCurrency.Token? {
val network = getNetwork(blockchain, responseDerivationPath, derivationStyleProvider)
?: return null
val id = getTokenId(network, sdkToken)
return CryptoCurrency.Token(
id = id,
network = getNetwork(blockchain) ?: return null,
network = network,
name = sdkToken.name,
symbol = sdkToken.symbol,
decimals = sdkToken.decimals,
derivationPath = derivationPath,
iconUrl = getTokenIconUrl(blockchain, sdkToken),
contractAddress = sdkToken.contractAddress,
isCustom = isCustomToken(id),
isCustom = isCustomToken(id, network),
)
}
}

View file

@ -2,14 +2,13 @@ package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
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.ID
import com.tangem.domain.tokens.models.Network
import com.tangem.blockchain.common.Token as SdkToken
import com.tangem.domain.tokens.models.CryptoCurrency.ID.Body as CurrencyIdBody
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
@ -18,24 +17,27 @@ private const val DEFAULT_TOKENS_ICONS_HOST = "https://s3.eu-central-1.amazonaws
private const val TOKEN_ICON_SIZE = "large"
private const val TOKEN_ICON_EXT = "png"
internal fun isCustomToken(tokenId: ID): Boolean {
return tokenId.rawCurrencyId == null
internal fun isCustomToken(tokenId: ID, network: Network): Boolean {
return network.derivationPath is Network.DerivationPath.Custom || tokenId.rawCurrencyId == null
}
internal fun getDerivationPath(blockchain: Blockchain, derivationStyleProvider: DerivationStyleProvider): String? {
return blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())?.rawPath
internal fun isCustomCoin(network: Network): Boolean {
return network.derivationPath is Network.DerivationPath.Custom
}
internal fun getBlockchain(networkId: Network.ID): Blockchain {
return Blockchain.fromId(networkId.value)
internal fun getCoinId(network: Network, coinId: String): ID {
return ID(COIN_ID_PREFIX, getCurrencyIdBody(network), CurrencyIdSuffix(rawId = coinId))
}
internal fun getCoinId(blockchain: Blockchain): ID {
return getTokenOrCoinId(blockchain, token = null)
}
internal fun getTokenId(network: Network, sdkToken: SdkToken): ID {
val sdkTokenId = sdkToken.id
val suffix = if (sdkTokenId == null) {
CustomCurrencyIdSuffix(contractAddress = sdkToken.contractAddress)
} else {
CurrencyIdSuffix(rawId = sdkTokenId)
}
internal fun getTokenId(blockchain: Blockchain, token: SdkToken): ID {
return getTokenOrCoinId(blockchain, token)
return ID(TOKEN_ID_PREFIX, getCurrencyIdBody(network), suffix)
}
internal fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? {
@ -58,15 +60,16 @@ internal fun getCoinIconUrl(blockchain: Blockchain): String? {
return coinId?.let(::getTokenIconUrlFromDefaultHost)
}
private fun getTokenOrCoinId(blockchain: Blockchain, token: SdkToken?): ID {
val sdkTokenId = token?.id
val (prefix, suffix) = when {
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)
private fun getCurrencyIdBody(network: Network): CurrencyIdBody {
return when (val path = network.derivationPath) {
is Network.DerivationPath.Custom -> CurrencyIdBody.NetworkIdWithDerivationPath(
rawId = network.id.value,
derivationPath = path.value,
)
is Network.DerivationPath.Card,
is Network.DerivationPath.None,
-> CurrencyIdBody.NetworkId(network.id.value)
}
return ID(prefix, Network.ID(blockchain.id), suffix)
}
private fun getTokenIconUrlFromDefaultHost(tokenId: String): String {

View file

@ -32,7 +32,7 @@ internal class UserTokensResponseFactory {
return UserTokensResponse.Token(
id = currency.id.rawCurrencyId,
networkId = blockchain.toNetworkId(),
derivationPath = currency.derivationPath,
derivationPath = currency.network.derivationPath.value,
name = currency.name,
symbol = currency.symbol,
decimals = currency.decimals,

View file

@ -19,12 +19,11 @@ class DefaultTxHistoryRepository(
private val userWalletsStore: UserWalletsStore,
) : TxHistoryRepository {
override suspend fun getTxHistoryItemsCount(networkId: Network.ID, derivationPath: String?): Int {
override suspend fun getTxHistoryItemsCount(network: Network): Int {
val userWallet = getUserWallet()
val state = walletManagersFacade.getTxHistoryState(
userWalletId = userWallet.walletId,
networkId = networkId,
rawDerivationPath = derivationPath,
network = network,
)
return when (state) {
is TxHistoryState.Failed.FetchError -> throw TxHistoryStateError.DataError(state.exception)
@ -34,11 +33,7 @@ class DefaultTxHistoryRepository(
}
}
override fun getTxHistoryItems(
networkId: Network.ID,
derivationPath: String?,
pageSize: Int,
): Flow<PagingData<TxHistoryItem>> {
override fun getTxHistoryItems(network: Network, pageSize: Int): Flow<PagingData<TxHistoryItem>> {
val userWallet = getUserWallet()
return Pager(
config = PagingConfig(
@ -49,8 +44,7 @@ class DefaultTxHistoryRepository(
loadPage = { page: Int, pageSize: Int ->
walletManagersFacade.getTxHistoryItems(
userWalletId = userWallet.walletId,
networkId = networkId,
rawDerivationPath = derivationPath,
network = network,
page = page,
pageSize = pageSize,
)

View file

@ -8,7 +8,6 @@ import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.hasDerivation
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.tokens.models.CryptoCurrency
@ -39,42 +38,46 @@ class DefaultWalletManagersFacade(
override suspend fun update(
userWalletId: UserWalletId,
networkId: Network.ID,
network: Network,
extraTokens: Set<CryptoCurrency.Token>,
): UpdateWalletManagerResult {
val userWallet = getUserWallet(userWalletId)
val blockchain = Blockchain.fromId(networkId.value)
val blockchain = Blockchain.fromId(network.id.value)
val derivationPath = network.derivationPath.value
return getAndUpdateWalletManager(userWallet, blockchain, extraTokens)
return getAndUpdateWalletManager(userWallet, blockchain, derivationPath, extraTokens)
}
override suspend fun getExploreUrl(userWalletId: UserWalletId, networkId: Network.ID): String {
override suspend fun getExploreUrl(userWalletId: UserWalletId, network: Network): String {
val userWallet = getUserWallet(userWalletId)
val blockchain = Blockchain.fromId(network.id.value)
val blockchain = Blockchain.fromId(networkId.value)
return getOrCreateWalletManager(
val walletManager = getOrCreateWalletManager(
userWallet = userWallet,
blockchain = blockchain,
derivationPath = blockchain
.derivationPath(userWallet.scanResponse.derivationStyleProvider.getDerivationStyle()),
derivationPath = network.derivationPath.value,
)
?.wallet
?.getExploreUrl()
.orEmpty()
}
override suspend fun getTxHistoryState(
userWalletId: UserWalletId,
networkId: Network.ID,
rawDerivationPath: String?,
): TxHistoryState {
val userWallet = getUserWallet(userWalletId)
val blockchain = Blockchain.fromId(networkId.value)
val derivationPath = rawDerivationPath?.let(::DerivationPath)
val walletManager = requireNotNull(getOrCreateWalletManager(userWallet, blockchain, derivationPath)) {
requireNotNull(walletManager) {
"Unable to get a wallet manager for blockchain: $blockchain"
}
return walletManager.wallet.getExploreUrl()
}
override suspend fun getTxHistoryState(userWalletId: UserWalletId, network: Network): TxHistoryState {
val userWallet = getUserWallet(userWalletId)
val blockchain = Blockchain.fromId(network.id.value)
val walletManager = getOrCreateWalletManager(
userWallet = userWallet,
blockchain = blockchain,
derivationPath = network.derivationPath.value,
)
requireNotNull(walletManager) {
"Unable to get a wallet manager for blockchain: $blockchain"
}
return walletManager
.getTransactionHistoryState(walletManager.wallet.address)
.let(txHistoryStateConverter::convert)
@ -82,17 +85,22 @@ class DefaultWalletManagersFacade(
override suspend fun getTxHistoryItems(
userWalletId: UserWalletId,
networkId: Network.ID,
rawDerivationPath: String?,
network: Network,
page: Int,
pageSize: Int,
): PaginationWrapper<TxHistoryItem> {
val userWallet = getUserWallet(userWalletId)
val blockchain = Blockchain.fromId(networkId.value)
val derivationPath = rawDerivationPath?.let(::DerivationPath)
val walletManager = requireNotNull(getOrCreateWalletManager(userWallet, blockchain, derivationPath)) {
val blockchain = Blockchain.fromId(network.id.value)
val walletManager = getOrCreateWalletManager(
userWallet = userWallet,
blockchain = blockchain,
derivationPath = network.derivationPath.value,
)
requireNotNull(walletManager) {
"Unable to get a wallet manager for blockchain: $blockchain"
}
val itemsResult = walletManager.getTransactionsHistory(
address = walletManager.wallet.address,
page = page,
@ -118,12 +126,12 @@ class DefaultWalletManagersFacade(
private suspend fun getAndUpdateWalletManager(
userWallet: UserWallet,
blockchain: Blockchain,
derivationPath: String?,
extraTokens: Set<CryptoCurrency.Token>,
): UpdateWalletManagerResult {
val scanResponse = userWallet.scanResponse
val derivationPath = blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle())
if (derivationPath != null && !scanResponse.hasDerivation(blockchain, derivationPath.rawPath)) {
if (derivationPath != null && !scanResponse.hasDerivation(blockchain, derivationPath)) {
Timber.e("Derivation missed for: $blockchain")
return UpdateWalletManagerResult.MissedDerivation
}
@ -171,21 +179,21 @@ class DefaultWalletManagersFacade(
override suspend fun getOrCreateWalletManager(
userWallet: UserWallet,
blockchain: Blockchain,
derivationPath: DerivationPath?,
derivationPath: String?,
): WalletManager? {
val userWalletId = userWallet.walletId
var walletManager = walletManagersStore.getSyncOrNull(
userWalletId = userWalletId,
blockchain = blockchain,
derivationPath = derivationPath?.rawPath,
derivationPath = derivationPath,
)
if (walletManager == null) {
walletManager = walletManagerFactory.createWalletManager(
scanResponse = userWallet.scanResponse,
blockchain = blockchain,
derivationPath = derivationPath,
derivationPath = derivationPath?.let { DerivationPath(rawPath = it) },
) ?: return null
walletManagersStore.store(userWalletId, walletManager)

View file

@ -2,7 +2,6 @@ package com.tangem.domain.walletmanager
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.txhistory.models.PaginationWrapper
@ -22,52 +21,53 @@ interface WalletManagersFacade {
* Updates the wallet manager associated with a user's wallet and network.
*
* @param userWalletId The ID of the user's wallet.
* @param networkId The network ID.
* @param network The network.
* @param extraTokens Additional tokens.
* @return The result of updating the wallet manager.
*/
suspend fun update(
userWalletId: UserWalletId,
networkId: Network.ID,
network: Network,
extraTokens: Set<CryptoCurrency.Token>,
): UpdateWalletManagerResult
suspend fun getExploreUrl(userWalletId: UserWalletId, networkId: Network.ID): String
/**
* Returns network explorer URL of the wallet manager associated with a user's wallet and network.
*
* @param userWalletId The ID of the user's wallet.
* @param network The network.
*
* @return The network explorer URL, maybe empty if the wallet manager was not found.
* */
suspend fun getExploreUrl(userWalletId: UserWalletId, network: Network): String
/**
* Returns transactions count
*
* @param userWalletId The ID of the user's wallet.
* @param networkId The network ID.
* @param rawDerivationPath Derivation path in raw form.
* @param network The network.
*/
suspend fun getTxHistoryState(
userWalletId: UserWalletId,
networkId: Network.ID,
rawDerivationPath: String?,
): TxHistoryState
suspend fun getTxHistoryState(userWalletId: UserWalletId, network: Network): TxHistoryState
/**
* Returns transaction history items wrapped to pagination
*
* @param userWalletId The ID of the user's wallet.
* @param networkId The network ID.
* @param rawDerivationPath Derivation path in raw form.
* @param network The network.
* @param page Pagination page.
* @param pageSize Pagination size.
*/
suspend fun getTxHistoryItems(
userWalletId: UserWalletId,
networkId: Network.ID,
rawDerivationPath: String?,
network: Network,
page: Int,
pageSize: Int,
): PaginationWrapper<TxHistoryItem>
// TODO: Remove after refactoring
suspend fun getOrCreateWalletManager(
userWallet: UserWallet,
blockchain: Blockchain,
derivationPath: DerivationPath?,
derivationPath: String?,
): WalletManager?
}

View file

@ -11,8 +11,7 @@ import java.io.Serializable
* @property symbol Symbol of the cryptocurrency.
* @property decimals Number of decimal places used by the cryptocurrency.
* @property iconUrl Optional URL of the cryptocurrency icon. `null` if not found.
* @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.
* @property isCustom Indicates whether the currency is a custom user-added currency or not.
*/
// FIXME: Remove serialization [REDACTED_JIRA]
sealed class CryptoCurrency : Serializable {
@ -23,7 +22,7 @@ sealed class CryptoCurrency : Serializable {
abstract val symbol: String
abstract val decimals: Int
abstract val iconUrl: String?
abstract val derivationPath: String?
abstract val isCustom: Boolean
/**
* Represents a native coin in the blockchain network.
@ -35,7 +34,7 @@ sealed class CryptoCurrency : Serializable {
override val symbol: String,
override val decimals: Int,
override val iconUrl: String?,
override val derivationPath: String?,
override val isCustom: Boolean,
) : CryptoCurrency() {
init {
@ -47,7 +46,6 @@ sealed class CryptoCurrency : Serializable {
* Represents a token in the blockchain network, typically a non-native asset.
*
* @property contractAddress Address of the contract managing the token.
* @property isCustom Indicates whether the token is a custom user-added token or not.
*/
data class Token(
override val id: ID,
@ -56,9 +54,8 @@ sealed class CryptoCurrency : Serializable {
override val symbol: String,
override val decimals: Int,
override val iconUrl: String?,
override val derivationPath: String?,
override val isCustom: Boolean,
val contractAddress: String,
val isCustom: Boolean,
) : CryptoCurrency() {
init {
@ -80,34 +77,70 @@ sealed class CryptoCurrency : Serializable {
// FIXME: Remove serialization [REDACTED_JIRA]
data class ID(
private val prefix: Prefix,
private val networkId: Network.ID,
private val body: Body,
private val suffix: Suffix,
) : Serializable {
val value: String = buildString {
append(prefix.value)
append(networkId.value)
append(DELIMITER)
append(PREFIX_DELIMITER)
append(body.value)
append(SUFFIX_DELIMITER)
append(suffix.value)
}
/** Represents a raw cryptocurrency ID. If it is a custom token, the value will be `null`. */
val rawCurrencyId: String? = (suffix as? Suffix.RawID)?.rawId
val rawNetworkId: String = networkId.value
/** Represents a raw cryptocurrency's network ID. */
val rawNetworkId: String = when (body) {
is Body.NetworkId -> body.rawId
is Body.NetworkIdWithDerivationPath -> body.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_"),
COIN_PREFIX(value = "coin"),
/** Prefix for standard tokens. */
TOKEN_PREFIX(value = "token_"),
TOKEN_PREFIX(value = "token"),
}
/** Prefix for custom tokens. */
CUSTOM_TOKEN_PREFIX(value = "custom_"),
/**
* Represents the body part of the cryptocurrency ID.
*
* The body can be either a raw network ID or a raw network ID with a network derivation path.
*/
sealed class Body {
/** The value of the body. */
abstract val value: String
/** Represents a raw network ID. */
data class NetworkId(val rawId: String) : Body() {
override val value: String = rawId
}
/**
* Represents a raw network ID with a network derivation path.
*
* Should be used for a cryptocurrencies with custom derivation path.
* */
data class NetworkIdWithDerivationPath(
val rawId: String,
val derivationPath: String,
) : Body() {
override val value: String = buildString {
append(rawId)
append(DERIVATION_PATH_DELIMITER)
append(derivationPath.hashCode())
}
}
}
/**
@ -132,8 +165,14 @@ sealed class CryptoCurrency : Serializable {
}
}
override fun toString(): String {
return "ID(value='$value')"
}
private companion object {
const val DELIMITER = '#'
const val PREFIX_DELIMITER = '_'
const val SUFFIX_DELIMITER = '#'
const val DERIVATION_PATH_DELIMITER = 'd'
}
}
@ -142,6 +181,5 @@ sealed class CryptoCurrency : Serializable {
require(symbol.isNotBlank()) { "Crypto currency symbol must not be blank" }
require(iconUrl?.isNotBlank() ?: true) { "Crypto currency icon URL must not be blank" }
require(decimals >= 0) { "Crypto currency decimal must not be less then zero, but it is: $decimals" }
require(derivationPath?.isNotBlank() ?: true) { "Crypto currency derivation path must not be blank" }
}
}

View file

@ -11,6 +11,7 @@ import java.io.Serializable
*
* @property id The unique identifier of the network.
* @property name The human-readable name of the network, such as "Ethereum" or "Bitcoin".
* @property derivationPath The path used to derive keys for this network.
* @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.
*/
@ -18,6 +19,7 @@ import java.io.Serializable
data class Network(
val id: ID,
val name: String,
val derivationPath: DerivationPath,
val isTestnet: Boolean,
val standardType: StandardType,
) : Serializable {
@ -39,6 +41,39 @@ data class Network(
}
}
/**
* Represents a path used to derive cryptographic keys for a blockchain network.
*
* This class represents such paths in a generic manner, allowing for predefined card-based paths,
* custom paths, or even no derivation path at all.
*/
sealed class DerivationPath {
/** The actual derivation path value, if any. */
abstract val value: String?
/**
* Represents a predefined card-based derivation path.
*
* @property value The derivation path string.
*/
data class Card(override val value: String) : DerivationPath()
/**
* Represents a custom derivation path specified by the user.
*
* @property value The derivation path string.
*/
data class Custom(override val value: String) : DerivationPath()
/**
* Represents a lack of derivation path.
*/
object None : DerivationPath() {
override val value: String? = null
}
}
/**
* Represents the type of blockchain standard that a network adheres to.
*

View file

@ -75,7 +75,7 @@ class FetchCurrencyStatusUseCase(
refresh: Boolean,
) = coroutineScope {
val fetchStatus = async {
fetchNetworkStatus(userWalletId, currency.network.id, refresh)
fetchNetworkStatus(userWalletId, currency.network, refresh)
}
val fetchQuote = async {
fetchQuote(currency.id, refresh)
@ -101,11 +101,11 @@ class FetchCurrencyStatusUseCase(
private suspend fun Raise<CurrencyStatusError>.fetchNetworkStatus(
userWalletId: UserWalletId,
networkId: Network.ID,
network: Network,
refresh: Boolean,
) {
catch(
block = { networksRepository.getNetworkStatusesSync(userWalletId, setOf(networkId), refresh) },
block = { networksRepository.getNetworkStatusesSync(userWalletId, setOf(network), refresh) },
) {
raise(CurrencyStatusError.DataError(it))
}

View file

@ -48,7 +48,7 @@ class FetchTokenListUseCase(
val fetchStatuses = async {
fetchNetworksStatuses(
userWalletId,
currencies.mapTo(hashSetOf()) { it.network.id },
currencies.mapTo(hashSetOf()) { it.network },
refresh,
)
}
@ -81,11 +81,11 @@ class FetchTokenListUseCase(
private suspend fun Raise<TokenListError>.fetchNetworksStatuses(
userWalletId: UserWalletId,
networksIds: Set<Network.ID>,
networks: Set<Network>,
refresh: Boolean,
) {
catch(
block = { networksRepository.getNetworkStatusesSync(userWalletId, networksIds, refresh) },
block = { networksRepository.getNetworkStatusesSync(userWalletId, networks, refresh) },
) {
raise(TokenListError.DataError(it))
}

View file

@ -27,10 +27,16 @@ class RemoveCurrencyUseCase(
}
suspend fun hasLinkedTokens(userWalletId: UserWalletId, currency: CryptoCurrency): Boolean {
val walletCurrencies = currenciesRepository
.getMultiCurrencyWalletCurrenciesSync(userWalletId = userWalletId, refresh = false)
return when (currency) {
is CryptoCurrency.Coin -> {
val walletCurrencies = currenciesRepository.getMultiCurrencyWalletCurrenciesSync(
userWalletId = userWalletId,
refresh = false,
)
return currency is CryptoCurrency.Coin &&
walletCurrencies.any { it != currency && it.network.id == currency.network.id }
walletCurrencies.any { it is CryptoCurrency.Token && it.network == currency.network }
}
is CryptoCurrency.Token -> false
}
}
}

View file

@ -7,7 +7,7 @@ import java.math.BigDecimal
/**
* Represents the status of a cryptocurrency asset within a network.
*
* This class encapsulates the details of a specific cryptocurrency, either a coin or token,
* This class encapsulates the details of a specific cryptocurrency, either a coin or cryptocurrency,
* along with its current status within the blockchain network. The status can include various states
* like Loading, Unreachable, Loaded, etc.
*
@ -20,22 +20,22 @@ data class CryptoCurrencyStatus(
) {
/**
* Represents the various states a token can have, encapsulating different information based on the state.
* Represents the various states a cryptocurrency can have, encapsulating different information based on the state.
*
* @property isError Indicates whether this status represents an error status.
*/
sealed class Status(val isError: Boolean) {
/** The amount of the token. */
/** The amount of the cryptocurrency. */
open val amount: BigDecimal? = null
/** The fiat equivalent of the token's amount. */
/** The fiat equivalent of the cryptocurrency's amount. */
open val fiatAmount: BigDecimal? = null
/** The exchange rate used for converting the token amount to fiat. */
/** The exchange rate used for converting the cryptocurrency amount to fiat. */
open val fiatRate: BigDecimal? = null
/** The change in price of the token. */
/** The change in price of the cryptocurrency. */
open val priceChange: BigDecimal? = null
/** Indicates if there are any transactions in progress related to the cryptocurrency network. */
@ -48,25 +48,28 @@ data class CryptoCurrencyStatus(
open val networkAddress: NetworkAddress? = null
}
/** Represents the Loading state of a token, typically while fetching its details. */
/** Represents the Loading state of a cryptocurrency, typically while fetching its details. */
object Loading : Status(isError = false)
/** Represents a state where the token is not reachable. */
/** Represents a state where the cryptocurrency is not reachable. */
object Unreachable : Status(isError = true)
/** Represents a state where the token's derivation is missed. */
/** Represents a state where the cryptocurrency's network amount not found. */
object NoAmount : Status(isError = true)
/** Represents a state where the cryptocurrency's derivation is missed. */
object MissedDerivation : Status(isError = true)
/** Represents a state where there is no account associated with the token. */
/** Represents a state where there is no account associated with the cryptocurrency. */
object NoAccount : Status(isError = false)
/**
* Represents a Loaded state of a token with complete information.
* Represents a Loaded state of a cryptocurrency with complete information.
*
* @property amount The amount of the token.
* @property fiatAmount The fiat equivalent of the token's amount.
* @property fiatRate The exchange rate used for converting the token amount to fiat.
* @property priceChange The change in price of the token.
* @property amount The amount of the cryptocurrency.
* @property fiatAmount The fiat equivalent of the cryptocurrency's amount.
* @property fiatRate The exchange rate used for converting the cryptocurrency amount to fiat.
* @property priceChange The change in price of the cryptocurrency.
* @property hasCurrentNetworkTransactions Indicates if there are any transactions in progress related to the
* cryptocurrency network.
* @property pendingTransactions The current cryptocurrency transactions.
@ -82,12 +85,12 @@ data class CryptoCurrencyStatus(
) : Status(isError = false)
/**
* Represents a Custom state of a token, typically used for user-defined tokens.
* Represents a Custom state of a cryptocurrency, typically used for user-defined tokens.
*
* @property amount The amount of the token.
* @property fiatAmount The fiat equivalent of the token's amount (optional).
* @property fiatRate The exchange rate used for converting the token amount to fiat (optional).
* @property priceChange The change in price of the token (optional).
* @property amount The amount of the cryptocurrency.
* @property fiatAmount The fiat equivalent of the cryptocurrency's amount (optional).
* @property fiatRate The exchange rate used for converting the cryptocurrency amount to fiat (optional).
* @property priceChange The change in price of the cryptocurrency (optional).
* @property hasCurrentNetworkTransactions Indicates if there are any transactions in progress related to the
* cryptocurrency network.
* @property pendingTransactions The current cryptocurrency transactions.
@ -103,9 +106,9 @@ data class CryptoCurrencyStatus(
) : Status(isError = false)
/**
* Represents a state where the token is available, but there is no current quote available for it.
* Represents a state where the cryptocurrency is available, but there is no current quote available for it.
*
* @property amount The amount of the token.
* @property amount The amount of the cryptocurrency.
* @property hasCurrentNetworkTransactions Indicates if there are any transactions in progress related to the
* cryptocurrency network.
* @property pendingTransactions The current cryptocurrency transactions.

View file

@ -8,11 +8,11 @@ import java.math.BigDecimal
/**
* Represents the status of a specific blockchain network.
*
* @property networkId The unique identifier of the network for which the status is provided.
* @property network The network for which the status is provided.
* @property value The specific status value, represented as a sealed class to encapsulate the various possible states of the network.
*/
data class NetworkStatus(
val networkId: Network.ID,
val network: Network,
val value: Status,
) {

View file

@ -58,11 +58,11 @@ internal class CurrenciesStatusesOperations(
emit(maybeLoadingCurrenciesStatuses)
val (networksIds, currenciesIds) = getIds(nonEmptyCurrencies)
val (networks, currenciesIds) = getIds(nonEmptyCurrencies)
val currenciesFlow = combine(
getQuotes(currenciesIds),
getNetworksStatuses(networksIds),
getNetworksStatuses(networks),
) { maybeQuotes, maybeNetworksStatuses ->
createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses)
}
@ -99,7 +99,7 @@ internal class CurrenciesStatusesOperations(
}
private fun getCurrencyStatusFlow(currency: CryptoCurrency): Flow<Either<Error, CryptoCurrencyStatus>> {
val (networksIds, currenciesIds) = getIds(nonEmptyListOf(currency))
val (networks, currenciesIds) = getIds(nonEmptyListOf(currency))
val quoteFlow = getQuotes(currenciesIds)
.map { maybeQuotes ->
@ -108,15 +108,15 @@ internal class CurrenciesStatusesOperations(
}
}
val statusFlow = getNetworksStatuses(networksIds)
val statusFlow = getNetworksStatuses(networks)
.map { maybeStatuses ->
maybeStatuses.map { statuses ->
statuses.singleOrNull { it.networkId == currency.network.id }
statuses.singleOrNull { it.network == currency.network }
}
}
return combine(quoteFlow, statusFlow) { maybeQuote, maybeNetworkStatus ->
createStatus(currency, maybeQuote, maybeNetworkStatus)
createCurrencyStatus(currency, maybeQuote, maybeNetworkStatus)
}
}
@ -135,13 +135,13 @@ internal class CurrenciesStatusesOperations(
currencies.map { currency ->
val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId }
val networkStatus = networksStatuses?.firstOrNull { it.networkId == currency.network.id }
val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network }
createStatus(currency, quote, networkStatus, ignoreQuote = quotesRetrievingFailed)
createCurrencyStatus(currency, quote, networkStatus, ignoreQuote = quotesRetrievingFailed)
}
}
private fun createStatus(
private fun createCurrencyStatus(
currency: CryptoCurrency,
maybeQuote: Either<Error, Quote?>,
maybeNetworkStatus: Either<Error, NetworkStatus?>,
@ -154,10 +154,10 @@ internal class CurrenciesStatusesOperations(
null
}
createStatus(currency, quote, networkStatus, ignoreQuote = quoteRetrievingFailed)
createCurrencyStatus(currency, quote, networkStatus, ignoreQuote = quoteRetrievingFailed)
}
private fun createStatus(
private fun createCurrencyStatus(
currency: CryptoCurrency,
quote: Quote?,
networkStatus: NetworkStatus?,
@ -206,7 +206,7 @@ internal class CurrenciesStatusesOperations(
.onEmpty { emit(Error.EmptyQuotes.left()) }
}
private fun getNetworksStatuses(networks: NonEmptySet<Network.ID>): Flow<Either<Error, Set<NetworkStatus>>> {
private fun getNetworksStatuses(networks: NonEmptySet<Network>): Flow<Either<Error, Set<NetworkStatus>>> {
return networksRepository.getNetworkStatusesUpdates(userWalletId, networks)
.map<Set<NetworkStatus>, Either<Error, Set<NetworkStatus>>> { it.right() }
.catch { emit(Error.DataError(it).left()) }
@ -215,17 +215,17 @@ internal class CurrenciesStatusesOperations(
private fun getIds(
currencies: NonEmptyList<CryptoCurrency>,
): Pair<NonEmptySet<Network.ID>, NonEmptySet<CryptoCurrency.ID>> {
): Pair<NonEmptySet<Network>, NonEmptySet<CryptoCurrency.ID>> {
val currencyIdToNetworkId = currencies.associate { currency ->
currency.id to currency.network.id
currency.id to currency.network
}
val currenciesIds = currencyIdToNetworkId.keys.toNonEmptySetOrNull()
val networksIds = currencyIdToNetworkId.values.toNonEmptySetOrNull()
val networks = currencyIdToNetworkId.values.toNonEmptySetOrNull()
requireNotNull(currenciesIds) { "Currencies IDs cannot be empty" }
requireNotNull(networksIds) { "Networks IDs cannot be empty" }
requireNotNull(networks) { "Networks IDs cannot be empty" }
return networksIds to currenciesIds
return networks to currenciesIds
}
sealed class Error {

View file

@ -26,7 +26,7 @@ internal class CurrencyStatusOperations(
}
private fun createStatus(status: NetworkStatus.Verified): CryptoCurrencyStatus.Status {
val amount = status.amounts[currency.id] ?: return CryptoCurrencyStatus.Unreachable
val amount = status.amounts[currency.id] ?: return CryptoCurrencyStatus.NoAmount
val hasCurrentNetworkTransactions = status.pendingTransactions.isNotEmpty()
val currentTransactions = status.pendingTransactions.getOrElse(currency.id, ::emptySet)

View file

@ -22,6 +22,7 @@ internal class TokenListFiatBalanceOperations(
}
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
-> {
fiatBalance = TokenList.FiatBalance.Failed
break

View file

@ -9,25 +9,16 @@ import kotlinx.coroutines.flow.Flow
* Repository for everything related to the blockchain networks
* */
interface NetworksRepository {
/**
* Retrieves the details of the specified blockchain networks, identified by their unique IDs.
*
* @param networksIds The unique identifiers of the networks to be retrieved.
* @return A set of [Network] objects corresponding to the specified network IDs.
*/
fun getNetworks(networksIds: Set<Network.ID>): Set<Network>
/**
* Retrieves updates of network statuses of specified blockchain networks for a specific user wallet.
*
* Loads remote network statuses if they have expired.
*
* @param userWalletId The unique identifier of the user wallet.
* @param networks A set of network IDs which statuses are to be retrieved.
* @param networks A set of network which statuses are to be retrieved.
* @return A [Flow] emitting a set of [NetworkStatus] objects corresponding to the specified networks.
*/
fun getNetworkStatusesUpdates(userWalletId: UserWalletId, networks: Set<Network.ID>): Flow<Set<NetworkStatus>>
fun getNetworkStatusesUpdates(userWalletId: UserWalletId, networks: Set<Network>): Flow<Set<NetworkStatus>>
/**
* Retrieves network statuses of specified blockchain networks for a specific user wallet.
@ -35,13 +26,13 @@ interface NetworksRepository {
* Loads remote network statuses if they have expired or if [refresh] is `true`.
*
* @param userWalletId The unique identifier of the user wallet.
* @param networks A set of network IDs which statuses are to be retrieved.
* @param networks A set of network which statuses are to be retrieved.
* @param refresh A boolean flag indicating whether the data should be refreshed.
* @return A [Flow] emitting a set of [NetworkStatus] objects corresponding to the specified networks.
*/
suspend fun getNetworkStatusesSync(
userWalletId: UserWalletId,
networks: Set<Network.ID>,
networks: Set<Network>,
refresh: Boolean,
): Set<NetworkStatus>
}

View file

@ -164,6 +164,6 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest {
isSortedByBalance = flowOf(),
),
quotesRepository = MockQuotesRepository(quotes),
networksRepository = MockNetworksRepository(MockNetworks.networks.right(), statuses),
networksRepository = MockNetworksRepository(statuses),
)
}

View file

@ -314,6 +314,6 @@ internal class GetTokenListUseCaseTest {
isSortedByBalance = isSortedByBalance,
),
quotesRepository = MockQuotesRepository(quotes),
networksRepository = MockNetworksRepository(MockNetworks.networks.right(), statuses),
networksRepository = MockNetworksRepository(statuses),
)
}

View file

@ -17,6 +17,7 @@ internal object MockNetworks {
name = "Network One",
isTestnet = false,
standardType = Network.StandardType.ERC20,
derivationPath = Network.DerivationPath.None,
)
val network2 = Network(
@ -24,6 +25,7 @@ internal object MockNetworks {
name = "Network Two",
isTestnet = false,
standardType = Network.StandardType.ERC20,
derivationPath = Network.DerivationPath.None,
)
val network3 = Network(
@ -31,22 +33,21 @@ internal object MockNetworks {
name = "Network Three",
isTestnet = false,
standardType = Network.StandardType.ERC20,
derivationPath = Network.DerivationPath.None,
)
val networks = nonEmptySetOf(network1, network2, network3)
val networkStatus1 = NetworkStatus(
networkId = network1.id,
network = network1,
value = NetworkStatus.Unreachable,
)
val networkStatus2 = NetworkStatus(
networkId = network2.id,
network = network2,
value = NetworkStatus.MissedDerivation,
)
val networkStatus3 = NetworkStatus(
networkId = network3.id,
network = network3,
value = NetworkStatus.NoAccount(
amountToCreateAccount = amountToCreateAccount,
address = NetworkAddress.Single(defaultAddress = "mock"),

View file

@ -7,17 +7,25 @@ internal object MockTokens {
val token1
get() = CryptoCurrency.Coin(
id = ID(ID.Prefix.COIN_PREFIX, MockNetworks.network1.id, ID.Suffix.RawID("token1")),
id = ID(
ID.Prefix.COIN_PREFIX,
ID.Body.NetworkId(MockNetworks.network1.id.value),
ID.Suffix.RawID("token1"),
),
network = MockNetworks.network1,
name = "Token 1",
symbol = "T1",
decimals = 8,
iconUrl = null,
derivationPath = null,
isCustom = false,
)
val token2
get() = CryptoCurrency.Token(
id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network1.id, ID.Suffix.RawID("token2")),
id = ID(
ID.Prefix.TOKEN_PREFIX,
ID.Body.NetworkId(MockNetworks.network1.id.value),
ID.Suffix.RawID("token2"),
),
network = MockNetworks.network1,
name = "Token 2",
symbol = "T2",
@ -25,11 +33,14 @@ internal object MockTokens {
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val token3
get() = CryptoCurrency.Token(
id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network1.id, ID.Suffix.RawID("token3")),
id = ID(
ID.Prefix.TOKEN_PREFIX,
ID.Body.NetworkId(MockNetworks.network1.id.value),
ID.Suffix.RawID("token3"),
),
network = MockNetworks.network1,
name = "Token 3",
symbol = "T3",
@ -37,21 +48,28 @@ internal object MockTokens {
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val token4
get() = CryptoCurrency.Coin(
id = ID(ID.Prefix.COIN_PREFIX, MockNetworks.network2.id, ID.Suffix.RawID("token4")),
id = ID(
ID.Prefix.COIN_PREFIX,
ID.Body.NetworkId(MockNetworks.network2.id.value),
ID.Suffix.RawID("token4"),
),
network = MockNetworks.network2,
name = "Token 4",
symbol = "T4",
decimals = 8,
iconUrl = null,
derivationPath = null,
isCustom = false,
)
val token5
get() = CryptoCurrency.Token(
id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network2.id, ID.Suffix.RawID("token5")),
id = ID(
ID.Prefix.TOKEN_PREFIX,
ID.Body.NetworkId(MockNetworks.network2.id.value),
ID.Suffix.RawID("token5"),
),
network = MockNetworks.network2,
name = "Token 5",
symbol = "T5",
@ -59,11 +77,14 @@ internal object MockTokens {
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val token6
get() = CryptoCurrency.Token(
id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network2.id, ID.Suffix.RawID("token6")),
id = ID(
ID.Prefix.TOKEN_PREFIX,
ID.Body.NetworkId(MockNetworks.network2.id.value),
ID.Suffix.RawID("token6"),
),
network = MockNetworks.network2,
name = "Token 6",
symbol = "T6",
@ -71,21 +92,28 @@ internal object MockTokens {
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val token7
get() = CryptoCurrency.Coin(
id = ID(ID.Prefix.COIN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token7")),
id = ID(
ID.Prefix.COIN_PREFIX,
ID.Body.NetworkId(MockNetworks.network3.id.value),
ID.Suffix.RawID("token7"),
),
network = MockNetworks.network3,
name = "Token 7",
symbol = "T7",
decimals = 8,
iconUrl = null,
derivationPath = null,
isCustom = false,
)
val token8
get() = CryptoCurrency.Token(
id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token8")),
id = ID(
ID.Prefix.TOKEN_PREFIX,
ID.Body.NetworkId(MockNetworks.network3.id.value),
ID.Suffix.RawID("token8"),
),
network = MockNetworks.network3,
name = "Token 8",
symbol = "T8",
@ -93,11 +121,14 @@ internal object MockTokens {
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val token9
get() = CryptoCurrency.Token(
id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token9")),
id = ID(
ID.Prefix.TOKEN_PREFIX,
ID.Body.NetworkId(MockNetworks.network3.id.value),
ID.Suffix.RawID("token9"),
),
network = MockNetworks.network3,
name = "Token 9",
symbol = "T9",
@ -105,11 +136,14 @@ internal object MockTokens {
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
val token10
get() = CryptoCurrency.Token(
id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token10")),
id = ID(
ID.Prefix.TOKEN_PREFIX,
ID.Body.NetworkId(MockNetworks.network3.id.value),
ID.Suffix.RawID("token10"),
),
network = MockNetworks.network3,
name = "Token 10",
symbol = "T10",
@ -117,7 +151,6 @@ internal object MockTokens {
decimals = 8,
iconUrl = null,
contractAddress = "address",
derivationPath = null,
)
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.network.id }
.first { it.network == status.currency.network }
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
@ -98,7 +98,7 @@ internal object MockTokensStates {
hasCurrentNetworkTransactions = false,
networkAddress = requireNotNull(
value = MockNetworks.verifiedNetworksStatuses
.first { it.networkId == status.currency.network.id }
.first { it.network == status.currency.network }
.value as? NetworkStatus.Verified,
).address,
),

View file

@ -11,24 +11,19 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
internal class MockNetworksRepository(
private val networks: Either<DataError, Set<Network>>,
private val statuses: Flow<Either<DataError, Set<NetworkStatus>>>,
) : NetworksRepository {
override fun getNetworks(networksIds: Set<Network.ID>): Set<Network> {
return networks.getOrElse { throw it }
}
override fun getNetworkStatusesUpdates(
userWalletId: UserWalletId,
networks: Set<Network.ID>,
networks: Set<Network>,
): Flow<Set<NetworkStatus>> {
return statuses.map { it.getOrElse { e -> throw e } }
}
override suspend fun getNetworkStatusesSync(
userWalletId: UserWalletId,
networks: Set<Network.ID>,
networks: Set<Network>,
refresh: Boolean,
): Set<NetworkStatus> {
return getNetworkStatusesUpdates(userWalletId, networks).first()

View file

@ -2,20 +2,16 @@ package com.tangem.domain.txhistory.repository
import androidx.paging.PagingData
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.txhistory.models.TxHistoryListError
import com.tangem.domain.txhistory.models.TxHistoryStateError
import com.tangem.domain.txhistory.models.TxHistoryItem
import kotlinx.coroutines.flow.Flow
interface TxHistoryRepository {
@Throws(TxHistoryStateError::class)
suspend fun getTxHistoryItemsCount(networkId: Network.ID, derivationPath: String?): Int
suspend fun getTxHistoryItemsCount(network: Network): Int
@Throws(TxHistoryListError::class)
fun getTxHistoryItems(
networkId: Network.ID,
derivationPath: String?,
pageSize: Int,
): Flow<PagingData<TxHistoryItem>>
fun getTxHistoryItems(network: Network, pageSize: Int): Flow<PagingData<TxHistoryItem>>
}

View file

@ -9,10 +9,10 @@ import com.tangem.domain.txhistory.repository.TxHistoryRepository
class GetTxHistoryItemsCountUseCase(private val repository: TxHistoryRepository) {
suspend operator fun invoke(networkId: Network.ID, derivationPath: String?): Either<TxHistoryStateError, Int> {
suspend operator fun invoke(network: Network): Either<TxHistoryStateError, Int> {
return either {
catch(
block = { repository.getTxHistoryItemsCount(networkId, derivationPath) },
block = { repository.getTxHistoryItemsCount(network) },
catch = { throwable ->
raise(
when (throwable) {

View file

@ -4,8 +4,8 @@ import androidx.paging.PagingData
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.txhistory.models.TxHistoryListError
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.txhistory.models.TxHistoryListError
import com.tangem.domain.txhistory.repository.TxHistoryRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
@ -15,13 +15,12 @@ private const val DEFAULT_PAGE_SIZE = 20
class GetTxHistoryItemsUseCase(private val repository: TxHistoryRepository) {
operator fun invoke(
networkId: Network.ID,
derivationPath: String?,
network: Network,
pageSize: Int = DEFAULT_PAGE_SIZE,
): Either<TxHistoryListError, Flow<PagingData<TxHistoryItem>>> {
return either {
repository
.getTxHistoryItems(networkId = networkId, derivationPath = derivationPath, pageSize = pageSize)
.getTxHistoryItems(network = network, pageSize = pageSize)
.catch { raise(TxHistoryListError.DataError(it)) }
}
}

View file

@ -1,12 +1,17 @@
package com.tangem.domain.wallets.usecase
import arrow.core.raise.catch
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
// TODO: Add tests
class GetExploreUrlUseCase(private val walletsManagersFacade: WalletManagersFacade) {
suspend operator fun invoke(userWalletId: UserWalletId, networkId: Network.ID): String {
return walletsManagersFacade.getExploreUrl(userWalletId, networkId)
// FIXME: Handle error
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): String {
return catch({ walletsManagersFacade.getExploreUrl(userWalletId, network) }) {
""
}
}
}

View file

@ -65,6 +65,7 @@ internal class TokenDetailsLoadedBalanceConverter(
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.NoAccount,
is CryptoCurrencyStatus.Custom,
is CryptoCurrencyStatus.NoAmount,
// TODO: [REDACTED_JIRA]
is CryptoCurrencyStatus.Unreachable,
-> {
@ -89,6 +90,7 @@ internal class TokenDetailsLoadedBalanceConverter(
is CryptoCurrencyStatus.Custom,
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.NoAccount,
is CryptoCurrencyStatus.NoAmount,
is CryptoCurrencyStatus.Unreachable,
-> MarketPriceBlockState.Error(currencyName)
}

View file

@ -118,8 +118,7 @@ internal class TokenDetailsViewModel @Inject constructor(
private fun updateTxHistory(refresh: Boolean = false) {
viewModelScope.launch(dispatchers.io) {
val txHistoryItemsCountEither = txHistoryItemsCountUseCase(
networkId = cryptoCurrency.network.id,
derivationPath = cryptoCurrency.derivationPath,
network = cryptoCurrency.network,
)
if (!refresh) {
@ -129,8 +128,7 @@ internal class TokenDetailsViewModel @Inject constructor(
txHistoryItemsCountEither.onRight {
uiState = stateFactory.getLoadedTxHistoryState(
txHistoryEither = txHistoryItemsUseCase(
networkId = cryptoCurrency.network.id,
derivationPath = cryptoCurrency.derivationPath,
network = cryptoCurrency.network,
).map {
it.cachedIn(viewModelScope)
},
@ -254,7 +252,7 @@ internal class TokenDetailsViewModel @Inject constructor(
router.openUrl(
url = getExploreUrlUseCase(
userWalletId = wallet.walletId,
networkId = cryptoCurrency.network.id,
network = cryptoCurrency.network,
),
)
}

View file

@ -206,7 +206,7 @@ internal object WalletPreviewData {
val networkNumber = index + 1
val group = DraggableItem.GroupHeader(
id = "group_$networkNumber",
id = networkNumber,
networkName = "$networkNumber",
roundingMode = when (index) {
0 -> DraggableItem.RoundingMode.Top()
@ -328,7 +328,7 @@ internal object WalletPreviewData {
walletsListConfig = walletListConfig,
tokensListState = WalletTokensListState.Content(
persistentListOf(
TokensListItemState.NetworkGroupTitle(TextReference.Str("Bitcoin")),
TokensListItemState.NetworkGroupTitle(id = 0, stringReference("Bitcoin")),
TokensListItemState.Token(
tokenItemVisibleState.copy(
id = "token_1",
@ -357,7 +357,7 @@ internal object WalletPreviewData {
amount = "1,89340821 ETH",
),
),
TokensListItemState.NetworkGroupTitle(TextReference.Str("Ethereum")),
TokensListItemState.NetworkGroupTitle(id = 1, stringReference("Ethereum")),
TokensListItemState.Token(
tokenItemVisibleState.copy(
id = "token_5",

View file

@ -13,7 +13,7 @@ import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
* */
@Immutable
internal sealed class DraggableItem {
abstract val id: String
abstract val id: Any
abstract val roundingMode: RoundingMode
abstract val showShadow: Boolean
@ -26,7 +26,7 @@ internal sealed class DraggableItem {
* @property showShadow if true then item should be elevated
* */
data class GroupHeader(
override val id: String,
override val id: Int,
val networkName: String,
override val roundingMode: RoundingMode = RoundingMode.None,
override val showShadow: Boolean = false,
@ -43,7 +43,7 @@ internal sealed class DraggableItem {
* */
data class Token(
val tokenItemState: TokenItemState.Draggable,
val groupId: String,
val groupId: Int,
override val showShadow: Boolean = false,
override val roundingMode: RoundingMode = RoundingMode.None,
) : DraggableItem() {

View file

@ -5,4 +5,4 @@ import com.tangem.domain.tokens.models.Network
internal fun getTokenItemId(currencyId: CryptoCurrency.ID): String = currencyId.value
internal fun getGroupHeaderId(networkId: Network.ID): String = networkId.value
internal fun getGroupHeaderId(network: Network): Int = network.hashCode()

View file

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

View file

@ -31,7 +31,7 @@ internal class NetworkGroupToDraggableItemsConverter(
}
private fun createGroupHeader(group: NetworkGroup) = DraggableItem.GroupHeader(
id = getGroupHeaderId(group.network.id),
id = getGroupHeaderId(group.network),
networkName = group.network.name,
)

View file

@ -7,7 +7,7 @@ import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteI
internal class DraggableGroupsOperations {
private var groupIdToTokens: Map<String, List<DraggableItem.Token>>? = null
private var groupIdToTokens: Map<Int, List<DraggableItem.Token>>? = null
fun collapseGroup(items: List<DraggableItem>, movingGroup: DraggableItem.GroupHeader): List<DraggableItem> {
if (!groupIdToTokens.isNullOrEmpty()) return items

View file

@ -53,7 +53,7 @@ internal sealed class WalletTokensListState {
/** Locked content state */
object Locked : ContentState(
items = persistentListOf(
TokensListItemState.NetworkGroupTitle(value = TextReference.Res(id = R.string.main_tokens)),
TokensListItemState.NetworkGroupTitle(id = 42, name = TextReference.Res(id = R.string.main_tokens)),
TokensListItemState.Token(state = TokenItemState.Locked(id = LOCKED_TOKEN_ID)),
),
organizeTokensButton = OrganizeTokensButtonState.Hidden,
@ -84,19 +84,26 @@ internal sealed class WalletTokensListState {
@Immutable
sealed interface TokensListItemState {
val id: Any
/**
* Network group title item
*
* @property value network name
* @property name network name
*/
data class NetworkGroupTitle(val value: TextReference) : TokensListItemState
data class NetworkGroupTitle(
override val id: Int,
val name: TextReference,
) : TokensListItemState
/**
* Token item
*
* @property state token item state
*/
data class Token(val state: TokenItemState) : TokensListItemState
data class Token(val state: TokenItemState) : TokensListItemState {
override val id: String = state.id
}
}
private companion object {

View file

@ -71,6 +71,7 @@ internal class WalletSingleCurrencyLoadedBalanceConverter(
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.NoAccount,
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
-> MarketPriceBlockState.Error(currencyName)
}
}
@ -112,6 +113,7 @@ internal class WalletSingleCurrencyLoadedBalanceConverter(
is CryptoCurrencyStatus.NoAccount,
is CryptoCurrencyStatus.Custom,
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
-> {
WalletCardState.Error(
id = selectedWallet.id,

View file

@ -44,12 +44,7 @@ private fun LazyListScope.contentItems(
) {
itemsIndexed(
items = items,
key = { _, item ->
when (item) {
is WalletTokensListState.TokensListItemState.NetworkGroupTitle -> item.value.hashCode()
is WalletTokensListState.TokensListItemState.Token -> item.state.id
}
},
key = { _, item -> item.id },
contentType = { _, item -> item::class.java },
itemContent = { index, item ->
MultiCurrencyContentItem(

View file

@ -19,7 +19,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.components.WalletToke
internal fun MultiCurrencyContentItem(state: WalletTokensListState.TokensListItemState, modifier: Modifier = Modifier) {
when (state) {
is WalletTokensListState.TokensListItemState.NetworkGroupTitle -> {
NetworkGroupItem(networkName = state.value.resolveReference(), modifier = modifier)
NetworkGroupItem(networkName = state.name.resolveReference(), modifier = modifier)
}
is WalletTokensListState.TokensListItemState.Token -> {
TokenItem(state = state.state, modifier = modifier)

View file

@ -30,6 +30,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter(
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.NoAccount,
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
-> value.mapToUnreachableTokenItemState()
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.feature.wallet.presentation.wallet.utils
import com.tangem.common.Provider
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.NetworkGroup
@ -68,7 +68,12 @@ internal class TokenListToContentItemsConverter(
}
private fun MutableList<TokensListItemState>.addGroup(group: NetworkGroup): List<TokensListItemState> {
this.add(TokensListItemState.NetworkGroupTitle(TextReference.Str(group.network.name)))
val groupTitle = TokensListItemState.NetworkGroupTitle(
id = group.network.hashCode(),
name = stringReference(group.network.name),
)
this.add(groupTitle)
group.currencies.forEach { token ->
this.addToken(token)

View file

@ -3,8 +3,6 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels
import androidx.lifecycle.*
import androidx.paging.cachedIn
import arrow.core.getOrElse
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.common.Provider
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
@ -15,7 +13,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.card.*
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.settings.CanUseBiometryUseCase
@ -461,14 +458,18 @@ internal class WalletViewModel @Inject constructor(
val wallet = getWallet(
index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex,
)
router.openTxHistoryWebsite(
url = getExploreUrlUseCase(
userWalletId = wallet.walletId,
networkId = Network.ID(
value = wallet.scanResponse.cardTypesResolver.getBlockchain().id,
val currencyStatus = getPrimaryCurrencyStatusUpdatesUseCase(wallet.walletId)
.firstOrNull()
?.getOrNull()
if (currencyStatus != null) {
router.openTxHistoryWebsite(
url = getExploreUrlUseCase(
userWalletId = wallet.walletId,
network = currencyStatus.currency.network,
),
),
)
)
}
}
}
@ -582,31 +583,20 @@ internal class WalletViewModel @Inject constructor(
private fun getSingleCurrencyContent(index: Int) {
val wallet = getWallet(index)
val blockchain = getCardTypeResolver(index).getBlockchain()
updateTxHistory(
blockchain = blockchain,
derivationStyle = wallet.scanResponse.derivationStyleProvider.getDerivationStyle(),
)
updateMarketPrice(userWalletId = wallet.walletId)
updatePrimaryCurrencyStatus(userWalletId = wallet.walletId)
updateNotifications(index)
}
private fun updateTxHistory(blockchain: Blockchain, derivationStyle: DerivationStyle?) {
private fun updateTxHistory(network: Network) {
viewModelScope.launch(dispatchers.io) {
val derivationPath = blockchain.derivationPath(style = derivationStyle)?.rawPath
val txHistoryItemsCountEither = txHistoryItemsCountUseCase(
networkId = Network.ID(blockchain.id),
derivationPath = derivationPath,
)
val txHistoryItemsCountEither = txHistoryItemsCountUseCase(network)
uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither)
txHistoryItemsCountEither.onRight {
uiState = stateFactory.getLoadedTxHistoryState(
txHistoryEither = txHistoryItemsUseCase(
networkId = Network.ID(blockchain.id),
derivationPath = derivationPath,
network,
).map {
it.cachedIn(viewModelScope)
},
@ -615,8 +605,7 @@ internal class WalletViewModel @Inject constructor(
}
}
// It also update wallet balance
private fun updateMarketPrice(userWalletId: UserWalletId) {
private fun updatePrimaryCurrencyStatus(userWalletId: UserWalletId) {
getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWalletId)
.distinctUntilChanged()
.onEach { maybeCryptoCurrencyStatus ->
@ -625,6 +614,7 @@ internal class WalletViewModel @Inject constructor(
maybeCryptoCurrencyStatus.onRight { status ->
singleWalletCryptoCurrencyStatus = status
updateButtons(userWalletId = userWalletId, currency = status.currency)
updateTxHistory(status.currency.network)
}
}
.flowOn(dispatchers.io)