diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index b593c7a762..ddf863762d 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -26,6 +26,17 @@ internal object TokensDomainModule { return GetTokenListUseCase(tokensRepository, quotesRepository, networksRepository, dispatchers) } + @Provides + @ViewModelScoped + fun provideGetTokenUseCase( + tokensRepository: TokensRepository, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + dispatchers: CoroutineDispatcherProvider, + ): GetTokenUseCase { + return GetTokenUseCase(tokensRepository, quotesRepository, networksRepository, dispatchers) + } + @Provides @ViewModelScoped fun provideToggleTokenListGroupingUseCase( diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt index 5fa21ece16..1c83a87d60 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt @@ -46,14 +46,31 @@ internal class DefaultTokensRepository( storeAndPushTokens(userWalletId, response) } - override fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> = channelFlow { + override suspend fun getSingleCurrencyWalletToken(userWalletId: UserWalletId): Token { val userWallet = withContext(dispatchers.io) { requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { "Unable to find a user wallet with provided ID: $userWalletId" } } + require(!userWallet.isMultiCurrency) { + "Single currency wallet excepted, but multi currency wallet was found: $userWalletId" + } + + return cardTokensFactory + .createPrimaryTokenForSingleCurrencyCard(userWallet.scanResponse) + } + + override fun getMultiCurrencyWalletTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> { + return channelFlow { + val userWallet = withContext(dispatchers.io) { + requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { + "Unable to find a user wallet with provided ID: $userWalletId" + } + } + require(!userWallet.isMultiCurrency) { + "Multi currency wallet excepted, but single currency wallet was found: $userWalletId" + } - if (userWallet.isMultiCurrency) { launch(dispatchers.io) { getMultiCurrencyWalletTokens(userWallet).collectLatest(::send) } @@ -61,8 +78,6 @@ internal class DefaultTokensRepository( launch(dispatchers.io) { fetchTokensIfCacheExpired(userWallet, refresh) } - } else { - send(getSingleCurrencyWalletTokens(userWallet)) } } @@ -87,22 +102,6 @@ internal class DefaultTokensRepository( } } - private suspend fun getSingleCurrencyWalletTokens(userWallet: UserWallet): Set { - var response = userTokensStore.getSyncOrNull(userWallet.walletId) - - if (response == null) { - response = userTokensResponseFactory.createUserTokensResponse( - tokens = cardTokensFactory.createTokensForSingleCurrencyCard(userWallet.scanResponse), - isGroupedByNetwork = false, - isSortedByBalance = false, - ) - - userTokensStore.store(userWallet.walletId, response) - } - - return responseTokensFactory.createTokens(response, userWallet.scanResponse.card) - } - private suspend fun fetchTokensIfCacheExpired(userWallet: UserWallet, refresh: Boolean) { cacheRegistry.invokeOnExpire( key = getTokensCacheKey(userWallet.walletId), diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt index 684a668b15..d41f73f91b 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt @@ -27,7 +27,7 @@ internal class CardTokensFactory(private val demoConfig: DemoConfig) { return blockchains.mapNotNull { createCoin(it, card) }.toSet() } - fun createTokensForSingleCurrencyCard(scanResponse: ScanResponse): Set { + fun createPrimaryTokenForSingleCurrencyCard(scanResponse: ScanResponse): DomainToken { val card = scanResponse.card val resolver = scanResponse.cardTypesResolver val blockchain = resolver.getBlockchain() @@ -39,13 +39,7 @@ internal class CardTokensFactory(private val demoConfig: DemoConfig) { createToken(token, blockchain, card) } - return buildSet { - add(coin) - - if (primaryToken != null) { - add(primaryToken) - } - } + return primaryToken ?: coin } private fun createToken(sdkToken: SdkToken, blockchain: Blockchain, card: CardDTO): DomainToken? { diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/error/DataError.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/error/DataError.kt index a76794c4c4..11567ca43b 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/error/DataError.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/error/DataError.kt @@ -6,11 +6,4 @@ sealed class DataError : Exception() { object NoInternetConnection : NetworkError() } - - sealed class PersistenceError : DataError() { - - data class UnableToReadFile(override val cause: Throwable) : DataError() - - data class UnableToWriteFile(override val cause: Throwable) : DataError() - } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt index fcd8578f39..9f07999ccd 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt @@ -18,6 +18,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.flatMapConcat class GetTokenListUseCase( @@ -31,7 +32,7 @@ class GetTokenListUseCase( return channelFlow { recover( block = { - getTokenList(userWalletId, refresh).collect { list -> + getTokenList(userWalletId, refresh).collectLatest { list -> send(list.right()) } }, @@ -59,7 +60,7 @@ class GetTokenListUseCase( transformError = TokensStatusesOperations.Error::mapToTokenListError, ) - return operations.getTokensStatusesFlow() + return operations.getMultiCurrencyWalletTokensStatusesFlow() } private fun Raise.createTokenList( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenUseCase.kt new file mode 100644 index 0000000000..fd3ea69f0b --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenUseCase.kt @@ -0,0 +1,57 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.left +import arrow.core.raise.Raise +import arrow.core.raise.recover +import arrow.core.right +import com.tangem.domain.tokens.error.TokenError +import com.tangem.domain.tokens.error.mapper.mapToTokenError +import com.tangem.domain.tokens.model.TokenStatus +import com.tangem.domain.tokens.operations.TokensStatusesOperations +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.tokens.repository.TokensRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collectLatest + +class GetTokenUseCase( + private val tokensRepository: TokensRepository, + private val quotesRepository: QuotesRepository, + private val networksRepository: NetworksRepository, + private val dispatchers: CoroutineDispatcherProvider, +) { + + operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = false): Flow> { + return channelFlow { + recover( + block = { + getToken(userWalletId, refresh).collectLatest { token -> + send(token.right()) + } + }, + recover = { error -> + send(error.left()) + }, + ) + } + } + + private suspend fun Raise.getToken(userWalletId: UserWalletId, refresh: Boolean): Flow { + val operations = TokensStatusesOperations( + tokensRepository = tokensRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + userWalletId = userWalletId, + refresh = refresh, + dispatchers = dispatchers, + raise = this, + transformError = TokensStatusesOperations.Error::mapToTokenError, + ) + + return operations.getSingleCurrencyWalletTokenStatusFlow() + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenError.kt new file mode 100644 index 0000000000..b8b3e0631b --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenError.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.tokens.error + +sealed class TokenError { + + object UnableToCreateToken : TokenError() + + data class DataError(val cause: Throwable) : TokenError() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/GetWalletTokenErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/GetWalletTokenErrorMappers.kt new file mode 100644 index 0000000000..25f2bf46d3 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/GetWalletTokenErrorMappers.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.tokens.error.mapper + +import com.tangem.domain.tokens.error.TokenError +import com.tangem.domain.tokens.operations.TokensStatusesOperations + +internal fun TokensStatusesOperations.Error.mapToTokenError(): TokenError { + return when (this) { + is TokensStatusesOperations.Error.DataError -> TokenError.DataError(this.cause) + is TokensStatusesOperations.Error.EmptyNetworksStatuses, + is TokensStatusesOperations.Error.EmptyQuotes, + is TokensStatusesOperations.Error.EmptyTokens, + -> TokenError.UnableToCreateToken + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenStatus.kt index 1ea03d8aa3..9a217c2f35 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenStatus.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenStatus.kt @@ -16,6 +16,7 @@ data class TokenStatus( sealed class Status { open val amount: BigDecimal? = null open val fiatAmount: BigDecimal? = null + open val fiatRate: BigDecimal? = null open val priceChange: BigDecimal? = null open val hasTransactionsInProgress: Boolean = false } @@ -31,6 +32,7 @@ data class TokenStatus( data class Loaded( override val amount: BigDecimal, override val fiatAmount: BigDecimal, + override val fiatRate: BigDecimal, override val priceChange: BigDecimal, override val hasTransactionsInProgress: Boolean, ) : Status() @@ -38,6 +40,7 @@ data class TokenStatus( data class Custom( override val amount: BigDecimal, override val fiatAmount: BigDecimal?, + override val fiatRate: BigDecimal?, override val priceChange: BigDecimal?, override val hasTransactionsInProgress: Boolean, ) : Status() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenStatusOperations.kt index dfb9d9f8bc..250ba9932f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenStatusOperations.kt @@ -48,6 +48,7 @@ internal class TokenStatusOperations( token.isCustom -> TokenStatus.Custom( amount = amount, fiatAmount = calculateFiatAmountOrNull(amount, quote?.fiatRate), + fiatRate = quote?.fiatRate, priceChange = quote?.priceChange, hasTransactionsInProgress = hasTransactionsInProgress, ) @@ -55,6 +56,7 @@ internal class TokenStatusOperations( else -> TokenStatus.Loaded( amount = amount, fiatAmount = calculateFiatAmount(amount, quote.fiatRate), + fiatRate = quote.fiatRate, priceChange = quote.priceChange, hasTransactionsInProgress = hasTransactionsInProgress, ) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokensStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokensStatusesOperations.kt index cbe9d303ad..7b7eced70a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokensStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokensStatusesOperations.kt @@ -1,8 +1,8 @@ package com.tangem.domain.tokens.operations -import arrow.core.NonEmptySet +import arrow.core.* import arrow.core.raise.Raise -import arrow.core.toNonEmptySetOrNull +import arrow.core.raise.catch import com.tangem.domain.core.raise.DelegatedRaise import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.model.* @@ -43,8 +43,8 @@ internal class TokensStatusesOperations( transformError = transformError, ) - fun getTokensStatusesFlow(): Flow> { - return getTokens().flatMapConcat { + fun getMultiCurrencyWalletTokensStatusesFlow(): Flow> { + return getMultiCurrencyWalletTokens().flatMapConcat { val tokens = it.toNonEmptySetOrNull() if (tokens == null) { @@ -60,6 +60,24 @@ internal class TokensStatusesOperations( } } + suspend fun getSingleCurrencyWalletTokenStatusFlow(): Flow { + val token = getSingleCurrencyWalletToken() + + val quoteFlow = getQuotes(nonEmptySetOf(token.id)) + .map { quotes -> + quotes.singleOrNull { it.tokenId == token.id } + } + + val statusFlow = getNetworksStatues(groupTokens(nonEmptySetOf(token))) + .map { statuses -> + statuses.singleOrNull { it.networkId == token.networkId } + } + + return combine(quoteFlow, statusFlow) { quote, networkStatus -> + createStatus(token, quote, networkStatus) + } + } + private suspend fun createTokensStatuses( tokens: Set, quotes: Set, @@ -84,13 +102,22 @@ internal class TokensStatusesOperations( return tokenStatusOperations.createTokenStatus() } - private fun getTokens(): Flow> { - return tokensRepository.getTokens(userWalletId, refresh) + private fun getMultiCurrencyWalletTokens(): Flow> { + return tokensRepository.getMultiCurrencyWalletTokens(userWalletId, refresh) .catch { raise(Error.DataError(it)) } .onEmpty { raise(Error.EmptyTokens) } .flowOn(dispatchers.io) } + private suspend fun getSingleCurrencyWalletToken(): Token { + return withContext(dispatchers.io) { + catch( + block = { tokensRepository.getSingleCurrencyWalletToken(userWalletId) }, + catch = { raise(Error.DataError(it)) }, + ) + } + } + private fun getQuotes(tokensIds: NonEmptySet): Flow> { return quotesRepository.getQuotes(tokensIds, refresh) .catch { raise(Error.DataError(it)) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt index 06e0b286b3..c019c0a34d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt @@ -16,7 +16,9 @@ interface TokensRepository { isSortedByBalance: Boolean, ) - fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> + suspend fun getSingleCurrencyWalletToken(userWalletId: UserWalletId): Token + + fun getMultiCurrencyWalletTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> fun isTokensGrouped(userWalletId: UserWalletId): Flow diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index b712bc465d..6652b1bd69 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -140,6 +140,7 @@ internal object MockTokensStates { value = TokenStatus.Loaded( amount = amount, fiatAmount = fiatAmount, + fiatRate = quote.fiatRate, priceChange = quote.priceChange, hasTransactionsInProgress = false, ), diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt index 02b76b06d5..bffa7f0ac5 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt @@ -3,6 +3,7 @@ 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.tokens.mock.MockTokens import com.tangem.domain.tokens.model.Token import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -37,7 +38,11 @@ internal class MockTokensRepository( isTokensSortedByBalanceAfterSortingApply = isSortedByBalance } - override fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> { + override suspend fun getSingleCurrencyWalletToken(userWalletId: UserWalletId): Token { + return MockTokens.token1 + } + + override fun getMultiCurrencyWalletTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> { return tokens.map { it.getOrElse { e -> throw e } } }