Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-25 13:35:18 +04:00
parent a335e8d8f4
commit fa125539b4
7 changed files with 165 additions and 12 deletions

View file

@ -8,9 +8,10 @@ import kotlinx.serialization.Serializable
/**
* Represents a list of account statuses associated with a user wallet
*
* @property userWallet the user wallet to which the account statuses belong
* @property accountStatuses a set of account statuses associated with the user wallet
* @property totalAccounts the total number of accounts
* @property userWallet the user wallet to which the account statuses belong
* @property accountStatuses a set of account statuses associated with the user wallet
* @property totalAccounts the total number of accounts (including archived ones)
* @property totalFiatBalance the total fiat balance across all accounts
*
[REDACTED_AUTHOR]
*/
@ -19,7 +20,7 @@ data class AccountStatusList(
val userWallet: UserWallet,
val accountStatuses: Set<AccountStatus>,
val totalAccounts: Int,
val totalFiatBalance: TotalFiatBalance = TotalFiatBalance.Failed,
val totalFiatBalance: TotalFiatBalance,
) {
val mainAccount: AccountStatus

View file

@ -5,6 +5,7 @@ import com.tangem.domain.core.utils.flatMap
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import kotlinx.serialization.Serializable
/**
* A sealed class representing the three states of a data load operation: Loading, Content, and Error.
@ -12,6 +13,7 @@ import com.tangem.domain.core.utils.lceLoading
* @param E The type of the error object.
* @param C The type of the content object.
*/
@Serializable
sealed class Lce<out E : Any, out C : Any> {
/**
@ -19,6 +21,7 @@ sealed class Lce<out E : Any, out C : Any> {
*
* @param partialContent The partial content that has been loaded so far, if any.
*/
@Serializable
data class Loading<C : Any>(val partialContent: C?) : Lce<Nothing, C>()
/**
@ -26,6 +29,7 @@ sealed class Lce<out E : Any, out C : Any> {
*
* @param content The loaded content.
*/
@Serializable
data class Content<C : Any>(val content: C) : Lce<Nothing, C>()
/**
@ -33,6 +37,7 @@ sealed class Lce<out E : Any, out C : Any> {
*
* @param error The error that occurred during loading.
*/
@Serializable
data class Error<E : Any>(val error: E) : Lce<E, Nothing>()
/**

View file

@ -1,5 +1,3 @@
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
plugins {
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.kotlin.serialization)
@ -13,6 +11,7 @@ tasks.withType<Test>().configureEach {
dependencies {
api(projects.domain.visa.models)
api(projects.domain.core)
api(projects.core.utils)
implementation(tangemDeps.card.core)

View file

@ -1,5 +1,7 @@
package com.tangem.domain.models.account
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.models.quote.PriceChange
import com.tangem.domain.models.tokenlist.TokenList
import kotlinx.serialization.Serializable
@ -17,12 +19,14 @@ sealed interface AccountStatus {
/**
* Represents the status of a crypto portfolio account
*
* @property account the crypto portfolio account
* @property tokenList the list of tokens associated with the account
* @property account the crypto portfolio account
* @property tokenList the list of tokens associated with the account
* @property priceChangeLce the price change information wrapped in a Lce (Loading, Content, Error) state
*/
@Serializable
data class CryptoPortfolio(
override val account: Account.CryptoPortfolio,
val tokenList: TokenList,
val priceChangeLce: Lce<Unit, PriceChange>,
) : AccountStatus
}

View file

@ -1,7 +1,8 @@
package com.tangem.domain.models.quote
import com.tangem.domain.models.StatusSource
import java.math.BigDecimal
import com.tangem.domain.models.serialization.SerializedBigDecimal
import kotlinx.serialization.Serializable
/**
* Represents the price change of a cryptocurrency asset over a specific time period.
@ -11,7 +12,8 @@ import java.math.BigDecimal
*
[REDACTED_AUTHOR]
*/
@Serializable
data class PriceChange(
val value: BigDecimal,
val value: SerializedBigDecimal,
val source: StatusSource,
)

View file

@ -1,11 +1,13 @@
package com.tangem.domain.tokens.operations
import arrow.core.NonEmptyList
import arrow.core.toNonEmptyListOrNull
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.getResultStatusSource
import com.tangem.domain.models.staking.YieldBalance
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.extensions.orZero
@ -37,6 +39,26 @@ object TotalFiatBalanceCalculator {
}
}
fun calculate(balances: List<TotalFiatBalance>): TotalFiatBalance {
val nonEmptyBalances = balances.toNonEmptyListOrNull()
?: return TokenList.Empty.totalFiatBalance
val computationState = ComputationState.resolve(balances = nonEmptyBalances)
return when (computationState) {
ComputationState.LOADING -> TotalFiatBalance.Loading
ComputationState.NON_COMPUTABLE -> TotalFiatBalance.Failed
ComputationState.COMPUTABLE -> {
val loaded = balances.filterIsInstance<TotalFiatBalance.Loaded>()
TotalFiatBalance.Loaded(
amount = loaded.sumOf(TotalFiatBalance.Loaded::amount),
source = loaded.map(TotalFiatBalance.Loaded::source).getResultStatusSource(),
)
}
}
}
private fun compute(statuses: NonEmptyList<CryptoCurrencyStatus>): TotalFiatBalance {
var mutableBalance: TotalFiatBalance = TotalFiatBalance.Loading
@ -189,6 +211,18 @@ object TotalFiatBalanceCalculator {
return COMPUTABLE
}
fun resolve(balances: List<TotalFiatBalance>): ComputationState {
for (balance in balances) {
when (balance) {
TotalFiatBalance.Failed -> return NON_COMPUTABLE
TotalFiatBalance.Loading -> return LOADING
is TotalFiatBalance.Loaded -> continue
}
}
return COMPUTABLE
}
}
}
}

View file

@ -31,7 +31,7 @@ class TotalFiatBalanceCalculatorTest {
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class LoadingOrNonComputable {
inner class CalculateCurrenciesLoadingOrNonComputable {
@Test
fun `one token is Loading, total is Loading`() {
@ -117,7 +117,7 @@ class TotalFiatBalanceCalculatorTest {
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class Computable {
inner class CalculateCurrenciesComputable {
@Test
fun `Unreachable token isIncludeToBalanceOnError, total is Loaded`() {
@ -377,6 +377,114 @@ class TotalFiatBalanceCalculatorTest {
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class CalculateBalancesLoadingOrNonComputable {
@Test
fun `one balance is Loading, total is Loading`() {
// Arrange
val balances = nonEmptyListOf(
TotalFiatBalance.Loading,
TotalFiatBalance.Failed,
)
// Act
val actual = TotalFiatBalanceCalculator.calculate(balances)
// Assert
val expected = TotalFiatBalance.Loading
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `one balance is Failed, total is Failed`() {
// Arrange
val balances = nonEmptyListOf(
TotalFiatBalance.Failed,
TotalFiatBalance.Loaded(amount = BigDecimal.ONE, source = StatusSource.ACTUAL),
)
// Act
val actual = TotalFiatBalanceCalculator.calculate(balances)
// Assert
val expected = TotalFiatBalance.Failed
Truth.assertThat(actual).isEqualTo(expected)
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class CalculateBalancesComputable {
@Test
fun `all balances are Loaded, total is Loaded`() {
// Arrange
val balances = nonEmptyListOf(
TotalFiatBalance.Loaded(amount = BigDecimal.TEN, source = StatusSource.ACTUAL),
TotalFiatBalance.Loaded(amount = BigDecimal.ONE, source = StatusSource.ACTUAL),
)
// Act
val actual = TotalFiatBalanceCalculator.calculate(balances)
// Assert
val expected = TotalFiatBalance.Loaded(
amount = BigDecimal(11),
source = StatusSource.ACTUAL,
)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `StatusSource is Actual is all balances are Actual`() {
// Arrange
val balances = nonEmptyListOf(
TotalFiatBalance.Loaded(amount = BigDecimal.TEN, source = StatusSource.ACTUAL),
TotalFiatBalance.Loaded(amount = BigDecimal.ONE, source = StatusSource.ACTUAL),
)
// Act
val actual = (TotalFiatBalanceCalculator.calculate(balances) as TotalFiatBalance.Loaded).source
// Assert
val expected = StatusSource.ACTUAL
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `StatusSource is Cache is any balance is Cache`() {
// Arrange
val balances = nonEmptyListOf(
TotalFiatBalance.Loaded(amount = BigDecimal.TEN, source = StatusSource.ACTUAL),
TotalFiatBalance.Loaded(amount = BigDecimal.ONE, source = StatusSource.CACHE),
)
// Act
val actual = (TotalFiatBalanceCalculator.calculate(balances) as TotalFiatBalance.Loaded).source
// Assert
val expected = StatusSource.CACHE
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `StatusSource is ONLY_CACHE is any balance is ONLY_CACHE`() {
// Arrange
val balances = nonEmptyListOf(
TotalFiatBalance.Loaded(amount = BigDecimal.TEN, source = StatusSource.ACTUAL),
TotalFiatBalance.Loaded(amount = BigDecimal.ONE, source = StatusSource.ONLY_CACHE),
)
// Act
val actual = (TotalFiatBalanceCalculator.calculate(balances) as TotalFiatBalance.Loaded).source
// Assert
val expected = StatusSource.ONLY_CACHE
Truth.assertThat(actual).isEqualTo(expected)
}
}
private fun createLoading(currency: CryptoCurrency): CryptoCurrencyStatus {
return CryptoCurrencyStatus(
currency = currency,