Updated on 2026-08-14
This commit is contained in:
parent
f2ab3a948d
commit
171732a41a
14 changed files with 162 additions and 45 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -46,14 +46,31 @@ internal class DefaultTokensRepository(
|
|||
storeAndPushTokens(userWalletId, response)
|
||||
}
|
||||
|
||||
override fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow<Set<Token>> = 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<Set<Token>> {
|
||||
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<Token> {
|
||||
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),
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ internal class CardTokensFactory(private val demoConfig: DemoConfig) {
|
|||
return blockchains.mapNotNull { createCoin(it, card) }.toSet()
|
||||
}
|
||||
|
||||
fun createTokensForSingleCurrencyCard(scanResponse: ScanResponse): Set<DomainToken> {
|
||||
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? {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TokenListError>.createTokenList(
|
||||
|
|
|
|||
|
|
@ -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<Either<TokenError, TokenStatus>> {
|
||||
return channelFlow {
|
||||
recover(
|
||||
block = {
|
||||
getToken(userWalletId, refresh).collectLatest { token ->
|
||||
send(token.right())
|
||||
}
|
||||
},
|
||||
recover = { error ->
|
||||
send(error.left())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun Raise<TokenError>.getToken(userWalletId: UserWalletId, refresh: Boolean): Flow<TokenStatus> {
|
||||
val operations = TokensStatusesOperations(
|
||||
tokensRepository = tokensRepository,
|
||||
quotesRepository = quotesRepository,
|
||||
networksRepository = networksRepository,
|
||||
userWalletId = userWalletId,
|
||||
refresh = refresh,
|
||||
dispatchers = dispatchers,
|
||||
raise = this,
|
||||
transformError = TokensStatusesOperations.Error::mapToTokenError,
|
||||
)
|
||||
|
||||
return operations.getSingleCurrencyWalletTokenStatusFlow()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.domain.tokens.error
|
||||
|
||||
sealed class TokenError {
|
||||
|
||||
object UnableToCreateToken : TokenError()
|
||||
|
||||
data class DataError(val cause: Throwable) : TokenError()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<E>(
|
|||
transformError = transformError,
|
||||
)
|
||||
|
||||
fun getTokensStatusesFlow(): Flow<Set<TokenStatus>> {
|
||||
return getTokens().flatMapConcat {
|
||||
fun getMultiCurrencyWalletTokensStatusesFlow(): Flow<Set<TokenStatus>> {
|
||||
return getMultiCurrencyWalletTokens().flatMapConcat {
|
||||
val tokens = it.toNonEmptySetOrNull()
|
||||
|
||||
if (tokens == null) {
|
||||
|
|
@ -60,6 +60,24 @@ internal class TokensStatusesOperations<E>(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun getSingleCurrencyWalletTokenStatusFlow(): Flow<TokenStatus> {
|
||||
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<Token>,
|
||||
quotes: Set<Quote>,
|
||||
|
|
@ -84,13 +102,22 @@ internal class TokensStatusesOperations<E>(
|
|||
return tokenStatusOperations.createTokenStatus()
|
||||
}
|
||||
|
||||
private fun getTokens(): Flow<Set<Token>> {
|
||||
return tokensRepository.getTokens(userWalletId, refresh)
|
||||
private fun getMultiCurrencyWalletTokens(): Flow<Set<Token>> {
|
||||
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<Token.ID>): Flow<Set<Quote>> {
|
||||
return quotesRepository.getQuotes(tokensIds, refresh)
|
||||
.catch { raise(Error.DataError(it)) }
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ interface TokensRepository {
|
|||
isSortedByBalance: Boolean,
|
||||
)
|
||||
|
||||
fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow<Set<Token>>
|
||||
suspend fun getSingleCurrencyWalletToken(userWalletId: UserWalletId): Token
|
||||
|
||||
fun getMultiCurrencyWalletTokens(userWalletId: UserWalletId, refresh: Boolean): Flow<Set<Token>>
|
||||
|
||||
fun isTokensGrouped(userWalletId: UserWalletId): Flow<Boolean>
|
||||
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ internal object MockTokensStates {
|
|||
value = TokenStatus.Loaded(
|
||||
amount = amount,
|
||||
fiatAmount = fiatAmount,
|
||||
fiatRate = quote.fiatRate,
|
||||
priceChange = quote.priceChange,
|
||||
hasTransactionsInProgress = false,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<Set<Token>> {
|
||||
override suspend fun getSingleCurrencyWalletToken(userWalletId: UserWalletId): Token {
|
||||
return MockTokens.token1
|
||||
}
|
||||
|
||||
override fun getMultiCurrencyWalletTokens(userWalletId: UserWalletId, refresh: Boolean): Flow<Set<Token>> {
|
||||
return tokens.map { it.getOrElse { e -> throw e } }
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue