Updated on 2026-08-14

This commit is contained in:
Tangem 2024-10-23 20:09:05 +04:00
commit 64a5db094e
30 changed files with 395 additions and 550 deletions

View file

@ -5,6 +5,7 @@ import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.extensions.addOrReplace
import com.tangem.utils.extensions.replaceBy
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@ -27,4 +28,23 @@ internal class DefaultNetworksStatusesStore(
store(key, newValues)
}
}
override suspend fun storeAll(key: UserWalletId, values: Collection<NetworkStatus>) {
mutex.withLock {
val currentValues = getSyncOrNull(key) ?: emptySet()
val updatedValues = currentValues.toMutableSet()
values.forEach { newValue ->
val isReplaced = updatedValues.replaceBy(newValue) {
it.network == newValue.network
}
if (!isReplaced) {
updatedValues.add(newValue)
}
}
store(key, updatedValues)
}
}
}

View file

@ -11,4 +11,6 @@ interface NetworksStatusesStore {
suspend fun getSyncOrNull(key: UserWalletId): Set<NetworkStatus>?
suspend fun store(key: UserWalletId, value: NetworkStatus)
suspend fun storeAll(key: UserWalletId, values: Collection<NetworkStatus>)
}

View file

@ -2,7 +2,6 @@ package com.tangem.data.staking
import android.util.Base64
import arrow.core.getOrElse
import arrow.core.raise.catch
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
@ -30,8 +29,6 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrap
import com.tangem.datasource.api.stakekit.models.response.model.transaction.tron.TronStakeKitTransaction
import com.tangem.datasource.local.token.StakingBalanceStore
import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.lce.lceFlow
import com.tangem.domain.staking.model.StakingApproval
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
@ -57,6 +54,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.orZero
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.withContext
import timber.log.Timber
@ -103,10 +101,6 @@ internal class DefaultStakingRepository(
private val yieldBalanceConverter = YieldBalanceConverter()
private val yieldBalanceListConverter = YieldBalanceListConverter(yieldBalanceConverter)
private val isYieldBalanceFetching = MutableStateFlow(
value = emptyMap<UserWalletId, Boolean>(),
)
private val tronStakeKitTransactionAdapter by lazy { moshi.adapter(TronStakeKitTransaction::class.java) }
override fun getIntegrationKey(cryptoCurrencyId: CryptoCurrency.ID): String = with(cryptoCurrencyId) {
@ -385,88 +379,66 @@ internal class DefaultStakingRepository(
refresh: Boolean,
) = withContext(dispatchers.io) {
if (!stakingFeatureToggle.isStakingEnabled) return@withContext
try {
isYieldBalanceFetching.update {
it + (userWalletId to true)
}
cacheRegistry.invokeOnExpire(
key = getYieldBalancesKey(userWalletId),
skipCache = refresh,
block = {
val yields = getEnabledYields()
val availableCurrencies = cryptoCurrencies
.mapNotNull { currency ->
val addresses = walletManagersFacade.getAddresses(userWalletId, currency.network)
val integrationId = integrationIdMap[getIntegrationKey(currency.id)]
if (integrationId != null && yields.any { it.id == integrationId }) {
addresses to integrationId
} else {
null
}
}
.flatMap { (addresses, integrationId) ->
addresses.map { address -> address to integrationId }
}
.map { getBalanceRequestData(it.first.value, it.second) }
.ifEmpty {
cacheRegistry.invalidate(getYieldBalancesKey(userWalletId))
error("No addresses found")
}
val result = stakeKitApi.getMultipleYieldBalances(availableCurrencies).getOrThrow()
cacheRegistry.invokeOnExpire(
key = getYieldBalancesKey(userWalletId),
skipCache = refresh,
block = {
val yields = getEnabledYields().ifEmpty {
Timber.i("No enabled yields for $userWalletId")
stakingBalanceStore.store(userWalletId, emptySet())
stakingBalanceStore.store(userWalletId, result)
},
)
} finally {
isYieldBalanceFetching.update {
it - userWalletId
}
}
return@invokeOnExpire
}
val availableCurrencies = cryptoCurrencies
.mapNotNull { currency ->
val addresses = walletManagersFacade.getAddresses(userWalletId, currency.network)
val integrationId = integrationIdMap[getIntegrationKey(currency.id)]
if (integrationId != null && yields.any { it.id == integrationId }) {
addresses to integrationId
} else {
null
}
}
.flatMap { (addresses, integrationId) ->
addresses.map { address -> address to integrationId }
}
.map { getBalanceRequestData(it.first.value, it.second) }
.ifEmpty {
Timber.i("No yield balances available for $userWalletId")
stakingBalanceStore.store(userWalletId, emptySet())
cacheRegistry.invalidate(getYieldBalancesKey(userWalletId))
return@invokeOnExpire
}
val result = stakeKitApi
.getMultipleYieldBalances(availableCurrencies)
.getOrThrow()
stakingBalanceStore.store(userWalletId, result)
},
)
}
override fun getMultiYieldBalanceFlow(
override fun getMultiYieldBalanceUpdates(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,
): Flow<YieldBalanceList> = channelFlow {
if (!stakingFeatureToggle.isStakingEnabled) {
send(YieldBalanceList.Empty)
} else {
launch(dispatchers.io) {
stakingBalanceStore.get(userWalletId)
.collectLatest { send(yieldBalanceListConverter.convert(it)) }
}
stakingBalanceStore.get(userWalletId)
.onEach {
val balances = yieldBalanceListConverter.convert(it)
send(balances)
}
.launchIn(scope = this + dispatchers.io)
withContext(dispatchers.io) {
fetchMultiYieldBalance(
userWalletId,
cryptoCurrencies,
)
}
}
}.cancellable()
override fun getMultiYieldBalanceLce(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,
): LceFlow<Throwable, YieldBalanceList> = lceFlow {
if (!stakingFeatureToggle.isStakingEnabled) {
send(YieldBalanceList.Empty)
} else {
launch(dispatchers.io) {
combine(
stakingBalanceStore.get(userWalletId),
isYieldBalanceFetching.map { it.getOrElse(userWalletId) { false } },
) { result, isFetching ->
val balances = yieldBalanceListConverter.convert(result)
send(balances, isStillLoading = isFetching)
}.collect()
}
withContext(dispatchers.io) {
catch(
block = { fetchMultiYieldBalance(userWalletId, cryptoCurrencies, refresh = false) },
catch = { raise(it) },
)
fetchMultiYieldBalance(userWalletId, cryptoCurrencies, refresh = false)
}
}
}

View file

@ -1,6 +1,5 @@
package com.tangem.data.tokens.repository
import arrow.core.raise.catch
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.compatibility.getL2CompatibilityTokenComparison
import com.tangem.blockchainsdk.utils.toCoinId
@ -27,8 +26,6 @@ import com.tangem.datasource.local.preferences.utils.storeObject
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.core.error.DataError
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.lce.lceFlow
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -42,6 +39,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.withContext
import timber.log.Timber
import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency
@ -66,10 +64,6 @@ internal class DefaultCurrenciesRepository(
private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility()
private val customTokensMerger = CustomTokensMerger(tangemTechApi, dispatchers)
private val isMultiCurrencyWalletCurrenciesFetching = MutableStateFlow(
value = emptyMap<UserWalletId, Boolean>(),
)
override suspend fun saveTokens(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
@ -206,18 +200,14 @@ internal class DefaultCurrenciesRepository(
}
}
override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): LceFlow<Throwable, List<CryptoCurrency>> {
return lceFlow {
val userWallet = catch({ getUserWallet(userWalletId) }) {
raise(it)
}
override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>> {
return channelFlow {
val userWallet = getUserWallet(userWalletId)
if (userWallet.isMultiCurrency) {
getMultiCurrencyWalletCurrenciesUpdatesLce(userWalletId).collect(::send)
getMultiCurrencyWalletCurrenciesUpdates(userWalletId).collect(::send)
} else {
val currency = catch({ getSingleCurrencyWalletPrimaryCurrency(userWalletId) }) {
raise(it)
}
val currency = getSingleCurrencyWalletPrimaryCurrency(userWalletId)
send(listOf(currency))
}
}
@ -260,40 +250,14 @@ internal class DefaultCurrenciesRepository(
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
launch(dispatchers.io) {
getMultiCurrencyWalletCurrencies(userWallet)
.collectLatest(::send)
}
getMultiCurrencyWalletCurrencies(userWallet)
.onEach { send(it) }
.launchIn(scope = this + dispatchers.io)
withContext(dispatchers.io) {
fetchTokensIfCacheExpired(userWallet, refresh = false)
}
}
.cancellable()
}
override fun getMultiCurrencyWalletCurrenciesUpdatesLce(
userWalletId: UserWalletId,
): LceFlow<Throwable, List<CryptoCurrency>> = lceFlow {
val userWallet = getUserWallet(userWalletId)
catch({ ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) }) {
raise(it)
}
launch(dispatchers.io) {
combine(
getMultiCurrencyWalletCurrencies(userWallet),
isMultiCurrencyWalletCurrenciesFetching.map { it.getOrElse(userWallet.walletId) { false } },
) { currencies, isFetching ->
send(currencies, isStillLoading = isFetching)
}.collect()
}
withContext(dispatchers.io) {
catch({ fetchTokensIfCacheExpired(userWallet, refresh = false) }) {
raise(it)
}
}
}
override suspend fun getMultiCurrencyWalletCurrenciesSync(
@ -545,19 +509,7 @@ internal class DefaultCurrenciesRepository(
cacheRegistry.invokeOnExpire(
key = getTokensCacheKey(userWallet.walletId),
skipCache = refresh,
block = {
isMultiCurrencyWalletCurrenciesFetching.update {
it + (userWallet.walletId to true)
}
try {
fetchTokens(userWallet)
} finally {
isMultiCurrencyWalletCurrenciesFetching.update {
it - userWallet.walletId
}
}
},
block = { fetchTokens(userWallet) },
)
}

View file

@ -1,6 +1,5 @@
package com.tangem.data.tokens.repository
import arrow.core.raise.catch
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.address.AddressType
import com.tangem.blockchainsdk.utils.fromNetworkId
@ -15,8 +14,6 @@ import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.lce.lceFlow
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
@ -28,7 +25,10 @@ import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
@Suppress("LongParameterList")
@ -46,42 +46,16 @@ internal class DefaultNetworksRepository(
private val responseCurrenciesFactory by lazy { ResponseCryptoCurrenciesFactory() }
private val networkStatusFactory by lazy { NetworkStatusFactory() }
private val isNetworkStatusesFetching = MutableStateFlow(
value = emptyMap<UserWalletId, Boolean>(),
)
override fun getNetworkStatusesUpdates(
userWalletId: UserWalletId,
networks: Set<Network>,
): Flow<Set<NetworkStatus>> = channelFlow {
launch(dispatchers.io) {
networksStatusesStore.get(userWalletId)
.collectLatest(::send)
}
networksStatusesStore.get(userWalletId)
.onEach(::send)
.launchIn(scope = this + dispatchers.io)
withContext(dispatchers.io) {
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, false)
}
}
.cancellable()
override fun getNetworkStatusesUpdatesLce(
userWalletId: UserWalletId,
networks: Set<Network>,
): LceFlow<Throwable, Set<NetworkStatus>> = lceFlow {
launch(dispatchers.io) {
combine(
networksStatusesStore.get(userWalletId),
isNetworkStatusesFetching.map { it.getOrElse(userWalletId) { false } },
) { statuses, isFetching ->
send(statuses, isStillLoading = isFetching)
}.collect()
}
withContext(dispatchers.io) {
catch({ fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh = false) }) {
raise(it)
}
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh = false)
}
}
@ -127,83 +101,36 @@ internal class DefaultNetworksRepository(
}
}
override suspend fun getNetworkAddress(
userWalletId: UserWalletId,
currency: CryptoCurrency,
): CryptoCurrencyAddress = withContext(dispatchers.io) {
CryptoCurrencyAddress(
cryptoCurrency = currency,
address = walletManagersFacade.getAddresses(userWalletId, currency.network)
.firstOrNull { it.type == AddressType.Default }
?.value.orEmpty(),
)
}
override fun getNetworkAddressFlow(
userWalletId: UserWalletId,
currency: CryptoCurrency,
): Flow<CryptoCurrencyAddress> = channelFlow {
launch(dispatchers.io) {
send(getNetworkAddress(userWalletId, currency))
}
}
override suspend fun getNetworkAddresses(userWalletId: UserWalletId): List<CryptoCurrencyAddress> =
withContext(dispatchers.io) {
// Get list of currencies matching [network]
val currencies = getCurrencies(userWalletId)
// There is no currencies matching given [networks] in [userWalletId]
if (currencies.toList().isEmpty()) return@withContext emptyList()
currencies.toList().map { currency ->
CryptoCurrencyAddress(
cryptoCurrency = currency,
address = walletManagersFacade.getAddresses(userWalletId, currency.network)
.firstOrNull { it.type == AddressType.Default }
?.value.orEmpty(),
)
}
}
override fun getNetworkAddressesFlow(
userWalletId: UserWalletId,
network: Network,
): Flow<List<CryptoCurrencyAddress>> = channelFlow {
launch(dispatchers.io) {
send(getNetworkAddresses(userWalletId, network))
}
}
override fun getNetworkAddressesFlow(userWalletId: UserWalletId): Flow<List<CryptoCurrencyAddress>> = channelFlow {
launch(dispatchers.io) {
send(getNetworkAddresses(userWalletId))
}
}
private suspend fun fetchNetworksStatusesIfCacheExpired(
userWalletId: UserWalletId,
networks: Set<Network>,
refresh: Boolean,
) {
val currencies = getCurrencies(userWalletId, networks)
val networksDeferred = networks.mapNotNull { network ->
fetchNetworkStatusIfCacheExpired(userWalletId, network, currencies, refresh)
) = coroutineScope {
if (refresh) {
val statusesToRefresh = networks.map { NetworkStatus(it, NetworkStatus.Refreshing) }
networksStatusesStore.storeAll(userWalletId, statusesToRefresh)
}
if (networksDeferred.isNotEmpty()) {
try {
isNetworkStatusesFetching.update {
it + (userWalletId to true)
}
val currencies = getCurrencies(userWalletId, networks)
val networksDeferred = networks.mapNotNull { network ->
coroutineScope {
val key = getNetworksStatusesCacheKey(userWalletId, network)
networksDeferred.awaitAll()
} finally {
isNetworkStatusesFetching.update {
it - userWalletId
if (refresh || cacheRegistry.isExpired(key)) {
async {
cacheRegistry.invokeOnExpire(
key = key,
skipCache = refresh,
block = { fetchNetworkStatus(userWalletId, network, currencies) },
)
}
} else {
null
}
}
}
networksDeferred.awaitAll()
}
private suspend fun fetchNetworksPendingTransactions(
@ -222,26 +149,6 @@ internal class DefaultNetworksRepository(
}
}
private suspend fun fetchNetworkStatusIfCacheExpired(
userWalletId: UserWalletId,
network: Network,
currencies: Sequence<CryptoCurrency>,
refresh: Boolean,
): Deferred<Unit>? = coroutineScope {
val key = getNetworksStatusesCacheKey(userWalletId, network)
if (refresh || cacheRegistry.isExpired(key)) {
async {
cacheRegistry.invokeOnExpire(
key = key,
skipCache = refresh,
block = { fetchNetworkStatus(userWalletId, network, currencies) },
)
}
} else {
null
}
}
private suspend fun fetchNetworkStatus(
userWalletId: UserWalletId,
network: Network,

View file

@ -1,5 +1,6 @@
package com.tangem.domain.core.lce
import arrow.atomic.AtomicBoolean
import arrow.core.raise.Raise
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
@ -34,6 +35,8 @@ class LceFlowScope<E : Any, C : Any> @PublishedApi internal constructor(
private val ifLoading: suspend LceFlowScope<E, C>.(C?) -> Unit,
) : Raise<E>, CoroutineScope by producerScope {
val isLoading: AtomicBoolean = AtomicBoolean(value = true)
/**
* Sends a error of type [E] within the [ProducerScope] and then closes it for send.
* All subsequent sends will be ignored.
@ -46,6 +49,8 @@ class LceFlowScope<E : Any, C : Any> @PublishedApi internal constructor(
* @param r Error to raise.
*/
override fun raise(r: E): Nothing {
isLoading.set(false)
producerScope.trySendBlocking(r.lceError())
producerScope.close()
@ -66,6 +71,8 @@ class LceFlowScope<E : Any, C : Any> @PublishedApi internal constructor(
* @param isStillLoading A flag indicating whether the content is still loading.
*/
suspend fun send(content: C, isStillLoading: Boolean = false) {
isLoading.set(isStillLoading)
val value = if (isStillLoading) {
ifLoading(content)
return
@ -89,6 +96,8 @@ class LceFlowScope<E : Any, C : Any> @PublishedApi internal constructor(
suspend fun send(value: Lce<E, C>) {
if (producerScope.isClosedForSend) return
isLoading.set(value.isLoading())
producerScope.send(value)
}
}

View file

@ -1,6 +1,8 @@
package com.tangem.domain.core.lce
import arrow.atomic.Atomic
import arrow.core.Either
import arrow.core.identity
import arrow.core.raise.Raise
import arrow.core.raise.RaiseDSL
import arrow.core.raise.recover
@ -97,6 +99,12 @@ class LceRaise<E : Any> @PublishedApi internal constructor(
is Lce.Content -> content
is Lce.Error -> raise(r = this)
}
@RaiseDSL
fun <C : Any> Either<E, C>.bindEither(): C = fold(
ifLeft = { raise(it) },
ifRight = ::identity,
)
}
/**

View file

@ -3,7 +3,6 @@ package com.tangem.domain.staking.repositories
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.staking.model.StakingApproval
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
@ -50,16 +49,11 @@ interface StakingRepository {
refresh: Boolean = false,
)
fun getMultiYieldBalanceFlow(
fun getMultiYieldBalanceUpdates(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,
): Flow<YieldBalanceList>
fun getMultiYieldBalanceLce(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,
): LceFlow<Throwable, YieldBalanceList>
suspend fun getMultiYieldBalanceSync(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,

View file

@ -21,6 +21,11 @@ data class NetworkStatus(
*/
sealed class Value
/**
* Represents the state where the network is refreshing.
*/
data object Refreshing : Value()
/**
* Represents the state where the network is unreachable.
*

View file

@ -40,38 +40,39 @@ class FetchTokenListUseCase(
* network statuses, and quotes for associated tokens.
*
* @param userWalletId The ID of the user's wallet.
* @param refresh Indicates whether to force a refresh of the token list data.
* @param mode The refresh mode to control the fetching process.
* @return An [Either] representing success (Right) or an error (Left) in fetching the token list.
*/
suspend operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = false): Either<TokenListError, Unit> {
return either {
val currencies = fetchCurrencies(userWalletId, refresh)
suspend operator fun invoke(
userWalletId: UserWalletId,
mode: RefreshMode = RefreshMode.NONE,
): Either<TokenListError, Unit> = either {
val currencies = fetchCurrencies(userWalletId, refresh = mode.refreshCurrencies)
coroutineScope {
val fetchStatuses = async {
fetchNetworksStatuses(
userWalletId,
currencies.mapTo(hashSetOf()) { it.network },
refresh,
)
}
val fetchQuotes = async {
fetchQuotes(
currencies.mapTo(hashSetOf()) { it.id },
refresh,
)
}
val yieldBalances = async {
fetchYieldBalances(
userWalletId = userWalletId,
currencies = currencies,
refresh = refresh,
)
}
awaitAll(fetchStatuses, fetchQuotes, yieldBalances)
coroutineScope {
val fetchStatuses = async {
fetchNetworksStatuses(
userWalletId,
currencies.mapTo(hashSetOf()) { it.network },
refresh = mode.refreshNetworksStatuses,
)
}
val fetchQuotes = async {
fetchQuotes(
currencies.mapTo(hashSetOf()) { it.id },
refresh = mode.refreshQuotes,
)
}
val yieldBalances = async {
fetchYieldBalances(
userWalletId = userWalletId,
currencies = currencies,
refresh = mode.refreshYieldBalances,
)
}
awaitAll(fetchStatuses, fetchQuotes, yieldBalances)
}
}
@ -120,4 +121,33 @@ class FetchTokenListUseCase(
catch = { /* Ignore error */ },
)
}
/**
* Represents the refresh modes available for fetching token list information.
*/
enum class RefreshMode(
internal val refreshCurrencies: Boolean,
internal val refreshNetworksStatuses: Boolean,
internal val refreshQuotes: Boolean,
internal val refreshYieldBalances: Boolean,
) {
NONE(
refreshCurrencies = false,
refreshNetworksStatuses = false,
refreshQuotes = false,
refreshYieldBalances = false,
),
FULL(
refreshCurrencies = true,
refreshNetworksStatuses = true,
refreshQuotes = true,
refreshYieldBalances = true,
),
SKIP_CURRENCIES(
refreshCurrencies = false,
refreshNetworksStatuses = true,
refreshQuotes = true,
refreshYieldBalances = true,
),
}
}

View file

@ -85,9 +85,6 @@ class GetWalletTotalBalanceUseCase(
stakingRepository = stakingRepository,
)
return operations.getCurrenciesStatuses(
userWalletId = userWalletId,
isSingleCurrencyWalletsAllowed = true,
)
return operations.getCurrenciesStatuses(userWalletId)
}
}

View file

@ -1,12 +1,13 @@
package com.tangem.domain.tokens.operations
import arrow.core.*
import arrow.core.raise.ensureNotNull
import arrow.core.raise.recover
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.lce.lce
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.core.lce.lceFlow
import com.tangem.domain.core.utils.EitherFlow
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.staking.model.stakekit.YieldBalanceList
import com.tangem.domain.staking.repositories.StakingRepository
@ -26,147 +27,126 @@ internal class CurrenciesStatusesLceOperations(
private val stakingRepository: StakingRepository,
) {
fun getCurrenciesStatuses(
userWalletId: UserWalletId,
isSingleCurrencyWalletsAllowed: Boolean = false,
): LceFlow<TokenListError, List<CryptoCurrencyStatus>> {
fun getCurrenciesStatuses(userWalletId: UserWalletId): LceFlow<TokenListError, List<CryptoCurrencyStatus>> {
return transformToCurrenciesStatuses(
userWalletId = userWalletId,
flow = if (isSingleCurrencyWalletsAllowed) {
getWalletCurrencies(userWalletId)
} else {
getMultiCurrencyWalletCurrencies(userWalletId)
},
currenciesFlow = getWalletCurrencies(userWalletId),
)
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun transformToCurrenciesStatuses(
userWalletId: UserWalletId,
flow: LceFlow<TokenListError, List<CryptoCurrency>>,
): LceFlow<TokenListError, List<CryptoCurrencyStatus>> {
return flow.transformLatest transform@{ maybeCurrencies ->
val nonEmptyCurrencies = maybeCurrencies.fold(
ifLoading = { maybeContent ->
emit(createLoadingCurrenciesStatuses(maybeContent))
return@transform
},
ifContent = { content ->
val nonEmptyCurrencies = content.toNonEmptyListOrNull()
currenciesFlow: EitherFlow<TokenListError, List<CryptoCurrency>>,
): LceFlow<TokenListError, List<CryptoCurrencyStatus>> = lceFlow {
currenciesFlow.collectLatest { maybeCurrencies ->
val nonEmptyCurrencies = maybeCurrencies.bind().toNonEmptyListOrNull()
ensureNotNull(nonEmptyCurrencies) { TokenListError.EmptyTokens }
if (nonEmptyCurrencies == null) {
emit(TokenListError.EmptyTokens.lceError())
return@transform
} else {
nonEmptyCurrencies
}
},
ifError = { error ->
emit(error.lceError())
return@transform
},
)
// This is only 'true' when the flow here is empty, such as during initial loading
if (isLoading.get()) {
val loadingCurrencies = createCurrenciesStatuses(
currencies = nonEmptyCurrencies,
maybeNetworkStatuses = null,
maybeQuotes = null,
maybeYieldBalances = null,
)
send(loadingCurrencies)
}
val (networks, currenciesIds) = getIds(nonEmptyCurrencies)
fun createCurrenciesStatuses(
maybeQuotes: Either<TokenListError, Set<Quote>>?,
maybeNetworkStatuses: Either<TokenListError, Set<NetworkStatus>>?,
maybeYieldBalances: Either<TokenListError, YieldBalanceList>?,
): Lce<TokenListError, List<CryptoCurrencyStatus>> = createCurrenciesStatuses(
currencies = nonEmptyCurrencies,
maybeQuotes = maybeQuotes,
maybeNetworkStatuses = maybeNetworkStatuses,
maybeYieldBalances = maybeYieldBalances,
)
combine(
getQuotes(currenciesIds),
getNetworksStatuses(userWalletId, networks),
getYieldBalances(userWalletId, nonEmptyCurrencies),
) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances ->
val statuses = createCurrenciesStatuses(
currencies = nonEmptyCurrencies,
maybeQuotes = maybeQuotes,
maybeNetworkStatuses = maybeNetworksStatuses,
maybeYieldBalances = maybeYieldBalances,
)
emit(statuses)
}.collect()
}
}
private fun createLoadingCurrenciesStatuses(
maybeCurrencies: List<CryptoCurrency>?,
): Lce<TokenListError, List<CryptoCurrencyStatus>> {
val nonEmptyCurrencies = maybeCurrencies?.toNonEmptyListOrNull()
val statuses = if (nonEmptyCurrencies == null) {
lceLoading()
} else {
createCurrenciesStatuses(
currencies = nonEmptyCurrencies,
maybeNetworkStatuses = null,
maybeQuotes = null,
maybeYieldBalances = null,
::createCurrenciesStatuses,
)
.distinctUntilChanged()
.mapLatest { maybeCurrenciesStatuses ->
send(maybeCurrenciesStatuses)
}
.launchIn(scope = this)
}
return statuses
}
private fun getWalletCurrencies(userWalletId: UserWalletId): LceFlow<TokenListError, List<CryptoCurrency>> {
private fun getWalletCurrencies(userWalletId: UserWalletId): EitherFlow<TokenListError, List<CryptoCurrency>> {
return currenciesRepository.getWalletCurrenciesUpdates(userWalletId)
.map { maybeCurrencies ->
maybeCurrencies.mapError { TokenListError.DataError(it) }
}
}
private fun getMultiCurrencyWalletCurrencies(
userWalletId: UserWalletId,
): LceFlow<TokenListError, List<CryptoCurrency>> {
return currenciesRepository.getMultiCurrencyWalletCurrenciesUpdatesLce(userWalletId)
.map<List<CryptoCurrency>, Either<TokenListError, List<CryptoCurrency>>> { it.right() }
.catch { emit(TokenListError.DataError(it).left()) }
.distinctUntilChanged()
.map { maybeCurrencies ->
maybeCurrencies.mapError { TokenListError.DataError(it) }
}
}
private fun createCurrenciesStatuses(
currencies: NonEmptyList<CryptoCurrency>,
maybeQuotes: Either<TokenListError, Set<Quote>>?,
maybeNetworkStatuses: Lce<TokenListError, Set<NetworkStatus>>?,
maybeYieldBalances: Lce<TokenListError, YieldBalanceList>?,
maybeNetworkStatuses: Either<TokenListError, Set<NetworkStatus>>?,
maybeYieldBalances: Either<TokenListError, YieldBalanceList>?,
): Lce<TokenListError, List<CryptoCurrencyStatus>> = lce {
isLoading.set(maybeNetworkStatuses == null)
isLoading.set(maybeNetworkStatuses == null || maybeYieldBalances == null)
var quotesRetrievingFailed = false
val networksStatuses = maybeNetworkStatuses?.bindOrNull()?.toNonEmptySetOrNull()
val networksStatuses = maybeNetworkStatuses?.bindEither()?.toNonEmptySetOrNull()
val yieldBalances = maybeYieldBalances?.bindEither()
val quotes = recover({ maybeQuotes?.bind()?.toNonEmptySetOrNull() }) {
quotesRetrievingFailed = true
null
}?.ifEmpty {
quotesRetrievingFailed = true
null
}
val yieldBalances = maybeYieldBalances?.getOrNull()
if (quotes == null) {
quotesRetrievingFailed = true
}
currencies.map { currency ->
val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId }
val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network }
val address = extractAddress(networkStatus)
val supportedIntegration = stakingRepository.getSupportedIntegrationId(currency.id)
val yieldBalance = if (supportedIntegration.isNullOrBlank().not()) {
(yieldBalances as? YieldBalanceList.Data)?.getBalance(
address = address,
integrationId = supportedIntegration,
)
} else {
null
}
val yieldBalance = findYieldBalanceOrNull(yieldBalances, currency, networkStatus)
createCurrencyStatus(
val currencyStatus = createCurrencyStatus(
currency = currency,
quote = quote,
networkStatus = networkStatus,
yieldBalance = yieldBalance,
ignoreQuote = quotesRetrievingFailed,
)
if (currencyStatus.value is CryptoCurrencyStatus.Loading) {
isLoading.set(true)
}
currencyStatus
}
}
private fun findYieldBalanceOrNull(
yieldBalances: YieldBalanceList?,
currency: CryptoCurrency,
networkStatus: NetworkStatus?,
): YieldBalance? {
if (yieldBalances !is YieldBalanceList.Data) return null
val supportedIntegration = stakingRepository.getSupportedIntegrationId(currency.id)
if (supportedIntegration.isNullOrBlank()) return null
return yieldBalances.getBalance(
address = extractAddress(networkStatus),
integrationId = supportedIntegration,
)
}
private fun createCurrencyStatus(
currency: CryptoCurrency,
quote: Quote?,
@ -189,28 +169,27 @@ internal class CurrenciesStatusesLceOperations(
return quotesRepository.getQuotesUpdates(tokensIds)
.map<Set<Quote>, Either<TokenListError, Set<Quote>>> { it.right() }
.catch { emit(TokenListError.DataError(it).left()) }
.distinctUntilChanged()
}
private fun getNetworksStatuses(
userWalletId: UserWalletId,
networks: NonEmptySet<Network>,
): LceFlow<TokenListError, Set<NetworkStatus>> {
return networksRepository.getNetworkStatusesUpdatesLce(userWalletId, networks)
.map { maybeStatuses ->
maybeStatuses.mapError { TokenListError.DataError(it) }
}
): EitherFlow<TokenListError, Set<NetworkStatus>> {
return networksRepository.getNetworkStatusesUpdates(userWalletId, networks)
.map<Set<NetworkStatus>, Either<TokenListError, Set<NetworkStatus>>> { it.right() }
.catch { emit(TokenListError.DataError(it).left()) }
.distinctUntilChanged()
}
private fun getYieldBalances(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,
): LceFlow<TokenListError, YieldBalanceList> {
return stakingRepository.getMultiYieldBalanceLce(
userWalletId = userWalletId,
cryptoCurrencies = cryptoCurrencies,
).map { maybeBalances ->
maybeBalances.mapError { TokenListError.DataError(it) }
}
): EitherFlow<TokenListError, YieldBalanceList> {
return stakingRepository.getMultiYieldBalanceUpdates(userWalletId, cryptoCurrencies)
.map<YieldBalanceList, Either<TokenListError, YieldBalanceList>> { it.right() }
.catch { emit(TokenListError.DataError(it).left()) }
.distinctUntilChanged()
}
private fun getIds(currencies: List<CryptoCurrency>): Pair<NonEmptySet<Network>, NonEmptySet<CryptoCurrency.ID>> {

View file

@ -16,7 +16,9 @@ internal class CurrencyStatusOperations(
private fun createStatus(): CryptoCurrencyStatus.Value {
return when (val status = networkStatus?.value) {
null -> CryptoCurrencyStatus.Loading
null,
is NetworkStatus.Refreshing,
-> CryptoCurrencyStatus.Loading
is NetworkStatus.MissedDerivation -> createMissedDerivationStatus()
is NetworkStatus.Unreachable -> createUnreachableStatus(status)
is NetworkStatus.NoAccount -> createNoAccountStatus(status)

View file

@ -1,7 +1,6 @@
package com.tangem.domain.tokens.repository
import com.tangem.domain.core.error.DataError
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.FeePaidCurrency
@ -81,7 +80,7 @@ interface CurrenciesRepository {
* @param userWalletId The unique identifier of the user wallet.
* @return A list of [CryptoCurrency].
*/
fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): LceFlow<Throwable, List<CryptoCurrency>>
fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>>
/**
* Retrieves the primary cryptocurrency for a specific single-currency user wallet.
@ -130,17 +129,6 @@ interface CurrenciesRepository {
*/
fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>>
/**
* Retrieves updates of the list of cryptocurrencies within a multi-currency wallet.
*
* Loads remote cryptocurrencies if they have expired.
*
* @param userWalletId The unique identifier of the user wallet.
* @return A [LceFlow] emitting the set of cryptocurrencies associated with the user wallet. May emit an
* [DataError.UserWalletError.WrongUserWallet] if single-currency user wallet ID provided.
*/
fun getMultiCurrencyWalletCurrenciesUpdatesLce(userWalletId: UserWalletId): LceFlow<Throwable, List<CryptoCurrency>>
/**
* Retrieves the list of cryptocurrencies within a multi-currency wallet.
*

View file

@ -1,7 +1,5 @@
package com.tangem.domain.tokens.repository
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus
@ -23,20 +21,6 @@ interface NetworksRepository {
*/
fun getNetworkStatusesUpdates(userWalletId: UserWalletId, networks: Set<Network>): Flow<Set<NetworkStatus>>
/**
* 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 which statuses are to be retrieved.
* @return A [LceFlow] emitting a set of [NetworkStatus] objects corresponding to the specified networks.
*/
fun getNetworkStatusesUpdatesLce(
userWalletId: UserWalletId,
networks: Set<Network>,
): LceFlow<Throwable, Set<NetworkStatus>>
/**
* Fetches pending transactions for given network
*
@ -63,33 +47,8 @@ interface NetworksRepository {
fun isNeedToCreateAccountWithoutReserve(network: Network): Boolean
/**
* Returns list of addresses and crypto currency info of added currencies of [network] in selected wallet [userWalletId]
*/
fun getNetworkAddressesFlow(userWalletId: UserWalletId, network: Network): Flow<List<CryptoCurrencyAddress>>
/**
* Returns list of addresses and crypto currency info of added currencies of [network] in selected wallet [userWalletId]
*/
suspend fun getNetworkAddresses(userWalletId: UserWalletId, network: Network): List<CryptoCurrencyAddress>
/**
* Returns address of [cryptoCurrency] in selected wallet [userWalletId]
*/
suspend fun getNetworkAddress(userWalletId: UserWalletId, currency: CryptoCurrency): CryptoCurrencyAddress
/**
* Returns address of [cryptoCurrency] in selected wallet [userWalletId]
*/
fun getNetworkAddressFlow(userWalletId: UserWalletId, currency: CryptoCurrency): Flow<CryptoCurrencyAddress>
/**
* Returns list of addresses and crypto currency info in selected wallet [userWalletId]
*/
fun getNetworkAddressesFlow(userWalletId: UserWalletId): Flow<List<CryptoCurrencyAddress>>
/**
* Returns list of addresses and crypto currency info in selected wallet [userWalletId]
*/
suspend fun getNetworkAddresses(userWalletId: UserWalletId): List<CryptoCurrencyAddress>
}

View file

@ -3,8 +3,6 @@ package com.tangem.domain.tokens.repository
import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.domain.core.error.DataError
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.utils.toLce
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.FeePaidCurrency
@ -57,7 +55,7 @@ internal class MockCurrenciesRepository(
override suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) = Unit
override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): LceFlow<Throwable, List<CryptoCurrency>> {
override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>> {
return emptyFlow()
}
@ -91,12 +89,6 @@ internal class MockCurrenciesRepository(
return tokens.map { it.getOrElse { e -> throw e } }
}
override fun getMultiCurrencyWalletCurrenciesUpdatesLce(
userWalletId: UserWalletId,
): LceFlow<Throwable, List<CryptoCurrency>> {
return tokens.map { it.toLce() }
}
override suspend fun getMultiCurrencyWalletCurrency(
userWalletId: UserWalletId,
id: CryptoCurrency.ID,

View file

@ -3,15 +3,11 @@ package com.tangem.domain.tokens.repository
import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.domain.core.error.DataError
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.utils.toLce
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
@ -26,13 +22,6 @@ internal class MockNetworksRepository(
return statuses.map { it.getOrElse { e -> throw e } }
}
override fun getNetworkStatusesUpdatesLce(
userWalletId: UserWalletId,
networks: Set<Network>,
): LceFlow<Throwable, Set<NetworkStatus>> {
return statuses.map { it.toLce() }
}
override suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set<Network>) {
// no-op
}
@ -47,37 +36,10 @@ internal class MockNetworksRepository(
override fun isNeedToCreateAccountWithoutReserve(network: Network) = false
override fun getNetworkAddressesFlow(
userWalletId: UserWalletId,
network: Network,
): Flow<List<CryptoCurrencyAddress>> = channelFlow {
send(emptyList())
}
override fun getNetworkAddressesFlow(userWalletId: UserWalletId): Flow<List<CryptoCurrencyAddress>> = channelFlow {
send(emptyList())
}
override suspend fun getNetworkAddresses(
userWalletId: UserWalletId,
network: Network,
): List<CryptoCurrencyAddress> {
return emptyList()
}
override suspend fun getNetworkAddresses(userWalletId: UserWalletId): List<CryptoCurrencyAddress> {
return emptyList()
}
override suspend fun getNetworkAddress(
userWalletId: UserWalletId,
currency: CryptoCurrency,
): CryptoCurrencyAddress = CryptoCurrencyAddress(currency, "")
override fun getNetworkAddressFlow(
userWalletId: UserWalletId,
currency: CryptoCurrency,
): Flow<CryptoCurrencyAddress> = channelFlow {
send(CryptoCurrencyAddress(currency, ""))
}
}

View file

@ -4,8 +4,6 @@ import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionStatus
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.lce.lceFlow
import com.tangem.domain.staking.model.StakingApproval
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
@ -20,6 +18,7 @@ import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.flowOf
import org.joda.time.DateTime
import java.math.BigDecimal
@ -146,27 +145,10 @@ class MockStakingRepository : StakingRepository {
/* no-op */
}
override fun getMultiYieldBalanceFlow(
override fun getMultiYieldBalanceUpdates(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,
): Flow<YieldBalanceList> = channelFlow {
send(
YieldBalanceList.Data(
balances = listOf(YieldBalance.Error),
),
)
}
override fun getMultiYieldBalanceLce(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,
): LceFlow<Throwable, YieldBalanceList> = lceFlow {
send(
YieldBalanceList.Data(
balances = listOf(YieldBalance.Error),
),
)
}
): Flow<YieldBalanceList> = flowOf()
override suspend fun getMultiYieldBalanceSync(
userWalletId: UserWalletId,

View file

@ -7,7 +7,6 @@ import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.promo.PromoBanner
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
import com.tangem.domain.settings.ShouldShowRingPromoUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -30,7 +29,7 @@ import kotlin.collections.count
@Suppress("LongParameterList")
@ViewModelScoped
internal class GetMultiWalletWarningsFactory @Inject constructor(
private val getTokenListUseCase: GetTokenListUseCase,
private val tokenListStore: MultiWalletTokenListStore,
private val isDemoCardUseCase: IsDemoCardUseCase,
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
private val shouldShowRingPromoUseCase: ShouldShowRingPromoUseCase,
@ -44,7 +43,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
val promoFlow = flow { emit(promoRepository.getRingPromoBanner()) }
return combine(
flow = getTokenListUseCase.launch(userWallet.walletId),
flow = tokenListStore.getOrThrow(userWallet.walletId),
flow2 = isReadyToShowRateAppUseCase(),
flow3 = isNeedToBackupUseCase(userWallet.walletId),
flow4 = shouldShowRingPromoUseCase(userWalletId = userWallet.walletId),
@ -129,6 +128,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
clickIntents: WalletClickIntents,
) {
val currencies = maybeTokenList.getMissingAddressCurrencies()
.ifEmpty { return }
addIf(
element = WalletNotification.Informational.MissingAddresses(

View file

@ -0,0 +1,62 @@
package com.tangem.feature.wallet.presentation.wallet.domain
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.wallets.models.UserWalletId
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.shareIn
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
@ViewModelScoped
internal class MultiWalletTokenListStore @Inject constructor(
private val getTokenListUseCase: GetTokenListUseCase,
) {
private val flows: ConcurrentHashMap<UserWalletId, LceFlow<TokenListError, TokenList>> by lazy {
ConcurrentHashMap()
}
fun addIfNot(userWalletId: UserWalletId, coroutineScope: CoroutineScope) {
if (flows[userWalletId] != null) {
Timber.d("Flow with token list for $userWalletId already exists")
return
}
coroutineScope.ensureActive()
flows[userWalletId] = getTokenListUseCase
.launch(userWalletId)
.shareIn(
scope = coroutineScope,
started = SharingStarted.WhileSubscribed(),
replay = 1,
)
Timber.d("Flow with token list for $userWalletId created")
}
fun getOrThrow(userWalletId: UserWalletId): LceFlow<TokenListError, TokenList> {
return requireNotNull(flows[userWalletId]) {
"Flow with token list for $userWalletId doesn't exist"
}
}
fun remove(userWalletId: UserWalletId) {
flows.remove(userWalletId)
Timber.d("Flow with token list for $userWalletId removed")
}
fun clear() {
flows.clear()
Timber.d("All flows with token list cleared")
}
}

View file

@ -2,12 +2,12 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletTokenListSubscriber
@ -23,7 +23,7 @@ internal class MultiWalletContentLoader(
private val tokenListAnalyticsSender: TokenListAnalyticsSender,
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
private val walletWithFundsChecker: WalletWithFundsChecker,
private val getTokenListUseCase: GetTokenListUseCase,
private val tokenListStore: MultiWalletTokenListStore,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
@ -38,7 +38,7 @@ internal class MultiWalletContentLoader(
clickIntents = clickIntents,
tokenListAnalyticsSender = tokenListAnalyticsSender,
walletWithFundsChecker = walletWithFundsChecker,
getTokenListUseCase = getTokenListUseCase,
tokenListStore = tokenListStore,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
applyTokenListSortingUseCase = applyTokenListSortingUseCase,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,

View file

@ -2,12 +2,12 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
@ -21,7 +21,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
private val tokenListAnalyticsSender: TokenListAnalyticsSender,
private val walletWithFundsChecker: WalletWithFundsChecker,
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
private val getTokenListUseCase: GetTokenListUseCase,
private val tokenListStore: MultiWalletTokenListStore,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
@ -35,7 +35,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
stateHolder = stateHolder,
tokenListAnalyticsSender = tokenListAnalyticsSender,
walletWithFundsChecker = walletWithFundsChecker,
getTokenListUseCase = getTokenListUseCase,
tokenListStore = tokenListStore,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
getMultiWalletWarningsFactory = getMultiWalletWarningsFactory,
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,

View file

@ -41,11 +41,11 @@ internal abstract class BasicTokenListSubscriber(
private val sendAnalyticsJobHolder = JobHolder()
private val onTokenListReceivedJobHolder = JobHolder()
protected abstract fun tokenListFlow(): LceFlow<TokenListError, TokenList>
protected abstract fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow<TokenListError, TokenList>
override fun create(coroutineScope: CoroutineScope): Flow<*> {
return combine(
flow = tokenListFlow()
flow = tokenListFlow(coroutineScope)
.onEach { maybeTokenList ->
coroutineScope.launch {
sendTokenListAnalytics(maybeTokenList)

View file

@ -4,7 +4,6 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrency
@ -12,14 +11,16 @@ import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import kotlinx.coroutines.CoroutineScope
@Suppress("LongParameterList")
internal class MultiWalletTokenListSubscriber(
private val userWallet: UserWallet,
private val getTokenListUseCase: GetTokenListUseCase,
private val tokenListStore: MultiWalletTokenListStore,
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
stateHolder: WalletStateController,
clickIntents: WalletClickIntents,
@ -37,8 +38,10 @@ internal class MultiWalletTokenListSubscriber(
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
) {
override fun tokenListFlow(): LceFlow<TokenListError, TokenList> {
return getTokenListUseCase.launch(userWallet.walletId)
override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow<TokenListError, TokenList> {
tokenListStore.addIfNot(userWallet.walletId, coroutineScope)
return tokenListStore.getOrThrow(userWallet.walletId)
}
override suspend fun onTokenListReceived(maybeTokenList: Lce<TokenListError, TokenList>) {

View file

@ -12,6 +12,7 @@ import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAn
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.map
@Suppress("LongParameterList")
@ -34,6 +35,9 @@ internal class SingleWalletWithTokenListSubscriber(
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
) {
override fun tokenListFlow(): LceFlow<TokenListError, TokenList> = getNodlTokenListUseCase(userWallet.walletId)
.map { it.toLce() }
override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow<TokenListError, TokenList> =
getNodlTokenListUseCase(
userWallet.walletId,
)
.map { it.toLce() }
}

View file

@ -16,6 +16,7 @@ import com.tangem.feature.wallet.presentation.deeplink.WalletDeepLinksHandler
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.SelectedWalletAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver
import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase
import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader
@ -69,6 +70,7 @@ internal class WalletViewModel @Inject constructor(
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
private val marketsFeatureToggles: MarketsFeatureToggles,
private val walletImageResolver: WalletImageResolver,
private val tokenListStore: MultiWalletTokenListStore,
analyticsEventsHandler: AnalyticsEventHandler,
) : ViewModel() {
@ -110,6 +112,8 @@ internal class WalletViewModel @Inject constructor(
override fun onCleared() {
super.onCleared()
tokenListStore.clear()
stateHolder.clear()
walletScreenContentLoader.cancelAll()
}
@ -280,7 +284,7 @@ internal class WalletViewModel @Inject constructor(
stateHolder.update(transformer = RenameWalletTransformer(action.selectedWalletId, action.name))
}
is WalletsUpdateActionResolver.Action.Unknown -> {
Timber.w("Unable to perfom action: $action")
Timber.w("Unable to perform action: $action")
}
}
}
@ -318,6 +322,7 @@ internal class WalletViewModel @Inject constructor(
private fun reinitializeWallet(action: WalletsUpdateActionResolver.Action.ReinitializeWallet) {
walletScreenContentLoader.cancel(action.prevWalletId)
tokenListStore.remove(action.prevWalletId)
walletScreenContentLoader.load(
userWallet = action.selectedWallet,
@ -357,6 +362,7 @@ internal class WalletViewModel @Inject constructor(
private suspend fun deleteWallet(action: WalletsUpdateActionResolver.Action.DeleteWallet) {
walletScreenContentLoader.cancel(action.deletedWalletId)
tokenListStore.remove(action.deletedWalletId)
walletScreenContentLoader.load(
userWallet = action.selectedWallet,

View file

@ -136,7 +136,9 @@ internal class WalletsUpdateActionResolver @Inject constructor(
unlockedWallets = wallets.filterNot(UserWallet::isLocked),
)
}
isSelectedWalletCardsCountChanged(state, selectedWallet) -> Action.UpdateWalletCardCount(selectedWallet)
isSelectedWalletCardsCountChanged(state, selectedWallet) -> {
Action.UpdateWalletCardCount(selectedWallet)
}
else -> Action.Unknown
}
}

View file

@ -12,6 +12,7 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.*
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertState
@ -37,6 +38,7 @@ internal interface WalletCardClickIntents {
@Suppress("LongParameterList")
internal class WalletCardClickIntentsImplementor @Inject constructor(
private val stateHolder: WalletStateController,
private val tokenListStore: MultiWalletTokenListStore,
private val walletEventSender: WalletEventSender,
private val walletScreenContentLoader: WalletScreenContentLoader,
private val renameWalletUseCase: RenameWalletUseCase,
@ -99,6 +101,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor(
override fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) {
viewModelScope.launch(dispatchers.main) {
walletScreenContentLoader.cancel(userWalletId)
tokenListStore.remove(userWalletId)
val walletToDelete = getUserWalletUseCase(userWalletId).getOrNull() ?: return@launch
val hasUserWallets = deleteWalletUseCase(userWalletId).getOrElse {
@ -117,6 +120,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor(
reduxStateHolder.onUserWalletSelected(selectedWallet)
} else {
tokenListStore.clear()
stateHolder.clear()
appRouter.replaceAll(AppRoute.Home)
}

View file

@ -8,6 +8,7 @@ import com.tangem.domain.settings.NeverToShowWalletsScrollPreview
import com.tangem.domain.tokens.FetchCardTokenListUseCase
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
import com.tangem.domain.tokens.FetchTokenListUseCase
import com.tangem.domain.tokens.FetchTokenListUseCase.RefreshMode
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.SelectWalletUseCase
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
@ -117,7 +118,7 @@ internal class WalletClickIntents @Inject constructor(
val maybeFetchResult = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
fetchCardTokenListUseCase(userWalletId = userWallet.walletId, refresh = true)
} else {
fetchTokenListUseCase(userWalletId = userWallet.walletId, refresh = true)
fetchTokenListUseCase(userWalletId = userWallet.walletId, mode = RefreshMode.FULL)
}
maybeFetchResult.onLeft {

View file

@ -13,6 +13,7 @@ import com.tangem.domain.redux.LegacyAction
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.settings.*
import com.tangem.domain.tokens.FetchTokenListUseCase
import com.tangem.domain.tokens.FetchTokenListUseCase.RefreshMode
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
@ -124,18 +125,20 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
analyticsEventHandler.send(Basic.CardWasScanned(AnalyticsParam.ScreensSources.Main))
analyticsEventHandler.send(MainScreen.NoticeScanYourCardTapped)
viewModelScope.launch(dispatchers.main) {
viewModelScope.launch {
val userWallet = getSelectedUserWallet() ?: return@launch
derivePublicKeysUseCase(
userWalletId = userWallet.walletId,
currencies = missedAddressCurrencies,
).fold(
ifLeft = { Timber.e(it, "Failed to derive public keys") },
ifRight = {
fetchTokenListUseCase(userWallet.walletId, mode = RefreshMode.SKIP_CURRENCIES).onLeft {
Timber.e("Unable to refresh token list: $it")
}
},
)
.onRight {
// Refresh must be set to true to ensure that yield balances are updated
fetchTokenListUseCase(userWalletId = userWallet.walletId, refresh = true)
}
.onLeft { Timber.e("Failed to derive public keys: $it") }
}
}