Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-18 16:34:01 +04:00
parent 5b25eb6312
commit 9f69ba143a
9 changed files with 708 additions and 134 deletions

View file

@ -21,6 +21,7 @@ dependencies {
implementation(projects.domain.models)
implementation(projects.domain.networks)
implementation(projects.domain.staking)
implementation(projects.domain.tokens)
implementation(projects.libs.crypto)

View file

@ -26,13 +26,12 @@ sealed interface TotalFiatBalance {
/**
* Represents the successfully loaded state of the fiat balance.
*
* @property amount the loaded fiat balance amount
* @property isAllAmountsSummarized indicates whether the amount includes a summary of all underlying amounts
* @property amount the loaded fiat balance amount
* @property source status source
*/
@Serializable
data class Loaded(
val amount: SerializedBigDecimal,
val isAllAmountsSummarized: Boolean,
val source: StatusSource,
) : TotalFiatBalance
}

View file

@ -70,7 +70,6 @@ sealed interface TokenList {
override val totalFiatBalance: TotalFiatBalance = TotalFiatBalance.Loaded(
amount = SerializedBigDecimal.ZERO,
isAllAmountsSummarized = true,
source = StatusSource.ACTUAL,
)

View file

@ -13,7 +13,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.tokens.operations.TokenListFiatBalanceOperations
import com.tangem.domain.tokens.operations.TotalFiatBalanceCalculator
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import timber.log.Timber
@ -112,11 +112,8 @@ class GetWalletTotalBalanceUseCase(
}
}
val operations = TokenListFiatBalanceOperations(
currencies = ensureNotNull(statuses.toNonEmptyListOrNull()) { lceLoading() },
isAnyTokenLoading = false,
TotalFiatBalanceCalculator.calculate(
statuses = ensureNotNull(statuses.toNonEmptyListOrNull()) { lceLoading() },
)
operations.calculateFiatBalance()
}
}

View file

@ -1,115 +0,0 @@
package com.tangem.domain.tokens.operations
import arrow.core.NonEmptyList
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.staking.utils.getTotalWithRewardsStakingBalance
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.extensions.orZero
import java.math.BigDecimal
internal class TokenListFiatBalanceOperations(
private val currencies: NonEmptyList<CryptoCurrencyStatus>,
private val isAnyTokenLoading: Boolean,
) {
@Suppress("LoopWithTooManyJumpStatements")
fun calculateFiatBalance(): TotalFiatBalance {
var fiatBalance: TotalFiatBalance = TotalFiatBalance.Loading
if (isAnyTokenLoading) return fiatBalance
for (token in currencies) {
val blockchainId = token.currency.network.rawId
when (val status = token.value) {
is CryptoCurrencyStatus.Loading -> {
fiatBalance = TotalFiatBalance.Loading
break
}
is CryptoCurrencyStatus.NoQuote,
is CryptoCurrencyStatus.MissedDerivation,
-> {
fiatBalance = TotalFiatBalance.Failed
break
}
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
-> {
if (BlockchainUtils.isIncludeToBalanceOnError(blockchainId)) {
fiatBalance = recalculateNoAccountBalance(status, fiatBalance)
} else {
fiatBalance = TotalFiatBalance.Failed
break
}
}
is CryptoCurrencyStatus.NoAccount -> {
fiatBalance = recalculateNoAccountBalance(status, fiatBalance)
}
is CryptoCurrencyStatus.Loaded -> {
fiatBalance = recalculateBalance(status, fiatBalance, blockchainId)
}
is CryptoCurrencyStatus.Custom -> {
fiatBalance = recalculateBalance(status, fiatBalance, blockchainId)
}
}
}
return (fiatBalance as? TotalFiatBalance.Loaded)?.copy(
source = currencies.map { it.value.sources.total }.getResultStatusSource(),
) ?: fiatBalance
}
private fun recalculateNoAccountBalance(
status: CryptoCurrencyStatus.Value,
currentBalance: TotalFiatBalance,
): TotalFiatBalance {
return (currentBalance as? TotalFiatBalance.Loaded)?.copy(isAllAmountsSummarized = false)
?: TotalFiatBalance.Loaded(
amount = BigDecimal.ZERO,
isAllAmountsSummarized = false,
source = (status as? CryptoCurrencyStatus.NoAccount)?.sources?.total ?: StatusSource.ACTUAL,
)
}
private fun recalculateBalance(
status: CryptoCurrencyStatus.Loaded,
currentBalance: TotalFiatBalance,
blockchainId: String,
): TotalFiatBalance {
return with(currentBalance) {
val yieldBalance = status.yieldBalance as? YieldBalance.Data
val stakingBalance = yieldBalance?.getTotalWithRewardsStakingBalance(blockchainId).orZero()
val fiatStakingBalance = status.fiatRate.times(stakingBalance)
(this as? TotalFiatBalance.Loaded)?.copy(
amount = this.amount + status.fiatAmount + fiatStakingBalance,
) ?: TotalFiatBalance.Loaded(
amount = status.fiatAmount + fiatStakingBalance,
isAllAmountsSummarized = true,
source = status.sources.total,
)
}
}
private fun recalculateBalance(
status: CryptoCurrencyStatus.Custom,
currentBalance: TotalFiatBalance,
blockchainId: String,
): TotalFiatBalance {
return with(currentBalance) {
val isTokenAmountCanBeSummarized = status.fiatAmount != null
val yieldBalance = (status.yieldBalance as? YieldBalance.Data)
?.getTotalWithRewardsStakingBalance(blockchainId).orZero()
val fiatYieldBalance = status.fiatRate?.times(yieldBalance).orZero()
(this as? TotalFiatBalance.Loaded)?.copy(
amount = this.amount + status.fiatAmount.orZero() + fiatYieldBalance,
isAllAmountsSummarized = isTokenAmountCanBeSummarized,
) ?: TotalFiatBalance.Loaded(
amount = status.fiatAmount.orZero() + fiatYieldBalance,
isAllAmountsSummarized = isTokenAmountCanBeSummarized,
source = StatusSource.ACTUAL,
)
}
}
}

View file

@ -33,13 +33,9 @@ internal class TokenListOperations(
private fun Raise<Error>.createTokenList(isGrouped: Boolean, isSortedByBalance: Boolean): TokenList {
val nonEmptyCurrencies = tokens.toNonEmptyListOrNull() ?: return TokenList.Empty
val isAnyTokenLoading = nonEmptyCurrencies.any { it.value is CryptoCurrencyStatus.Loading }
val fiatBalanceOperations = TokenListFiatBalanceOperations(nonEmptyCurrencies, isAnyTokenLoading)
return createTokenList(
currencies = nonEmptyCurrencies,
fiatBalance = fiatBalanceOperations.calculateFiatBalance(),
isAnyTokenLoading = isAnyTokenLoading,
fiatBalance = TotalFiatBalanceCalculator.calculate(statuses = nonEmptyCurrencies),
isGrouped = isGrouped,
isSortedByBalance = isSortedByBalance,
)
@ -48,13 +44,12 @@ internal class TokenListOperations(
private fun Raise<Error>.createTokenList(
currencies: NonEmptyList<CryptoCurrencyStatus>,
fiatBalance: TotalFiatBalance,
isAnyTokenLoading: Boolean,
isGrouped: Boolean,
isSortedByBalance: Boolean,
): TokenList {
val sortingOperations = TokenListSortingOperations(
currencies = currencies,
isAnyTokenLoading = isAnyTokenLoading,
isAnyTokenLoading = currencies.any { it.value is CryptoCurrencyStatus.Loading },
sortByBalance = isSortedByBalance,
)

View file

@ -0,0 +1,194 @@
package com.tangem.domain.tokens.operations
import arrow.core.NonEmptyList
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.staking.utils.getTotalWithRewardsStakingBalance
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.extensions.orZero
import java.math.BigDecimal
/**
* Utility to calculate total fiat balance from a set of [CryptoCurrencyStatus].
*
* The calculation considers various states of each cryptocurrency, including loading,
* unreachable, no amount, no account, loaded, and custom states.
*
* The result is a [TotalFiatBalance], which can be in one of the following states:
* - [TotalFiatBalance.Loading]: If any cryptocurrency is still loading.
* - [TotalFiatBalance.Failed]: If any cryptocurrency is in a non-computable state (e.g., no quote).
* - [TotalFiatBalance.Loaded]: If all cryptocurrencies are computable, containing the total fiat amount,
* a flag indicating if all amounts were summarized, and the source of the data.
*
* The calculation also takes into account staking balances when available.
*/
object TotalFiatBalanceCalculator {
fun calculate(statuses: NonEmptyList<CryptoCurrencyStatus>): TotalFiatBalance {
val computationState = ComputationState.resolve(statuses)
return when (computationState) {
ComputationState.LOADING -> TotalFiatBalance.Loading
ComputationState.NON_COMPUTABLE -> TotalFiatBalance.Failed
ComputationState.COMPUTABLE -> compute(statuses)
}
}
private fun compute(statuses: NonEmptyList<CryptoCurrencyStatus>): TotalFiatBalance {
var mutableBalance: TotalFiatBalance = TotalFiatBalance.Loading
for (token in statuses) {
val blockchainId = token.currency.network.rawId
when (val status = token.value) {
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
is CryptoCurrencyStatus.NoAccount,
-> {
mutableBalance = mutableBalance.plusEmptyBalance(status)
}
is CryptoCurrencyStatus.Loaded -> {
mutableBalance = mutableBalance.plusLoaded(status, blockchainId)
}
is CryptoCurrencyStatus.Custom -> {
mutableBalance = mutableBalance.plusLoaded(status, blockchainId)
}
// Non computable states, should be handled before
CryptoCurrencyStatus.Loading,
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.NoQuote,
-> continue
}
}
return mutableBalance.updateSource(statuses)
}
private fun TotalFiatBalance.plusEmptyBalance(status: CryptoCurrencyStatus.Value): TotalFiatBalance {
return fold(
ifLoaded = { it },
ifNot = {
TotalFiatBalance.Loaded(
amount = BigDecimal.ZERO,
source = status.sources.total, // never mind
)
},
)
}
private fun TotalFiatBalance.plusLoaded(
status: CryptoCurrencyStatus.Loaded,
blockchainId: String,
): TotalFiatBalance {
val fiatStakingBalance = status.getFiatStakingBalance(blockchainId)
return fold(
ifLoaded = { loaded ->
loaded.copy(amount = loaded.amount + status.fiatAmount + fiatStakingBalance)
},
ifNot = {
TotalFiatBalance.Loaded(
amount = status.fiatAmount + fiatStakingBalance,
source = status.sources.total, // never mind
)
},
)
}
private fun TotalFiatBalance.plusLoaded(
status: CryptoCurrencyStatus.Custom,
blockchainId: String,
): TotalFiatBalance {
val fiatStakingBalance = status.getFiatStakingBalance(blockchainId)
return fold(
ifLoaded = { loaded ->
loaded.copy(
amount = loaded.amount + status.fiatAmount.orZero() + fiatStakingBalance,
)
},
ifNot = {
TotalFiatBalance.Loaded(
amount = status.fiatAmount.orZero() + fiatStakingBalance,
source = StatusSource.ACTUAL,
)
},
)
}
private fun TotalFiatBalance.updateSource(statuses: NonEmptyList<CryptoCurrencyStatus>): TotalFiatBalance {
return fold(
ifLoaded = { loaded ->
loaded.copy(
source = statuses.map { it.value.sources.total }.getResultStatusSource(),
)
},
ifNot = { this },
)
}
private fun CryptoCurrencyStatus.Loaded.getFiatStakingBalance(blockchainId: String): BigDecimal {
val yieldBalance = yieldBalance as? YieldBalance.Data
val stakingBalance = yieldBalance?.getTotalWithRewardsStakingBalance(blockchainId).orZero()
return fiatRate.times(stakingBalance)
}
private fun CryptoCurrencyStatus.Custom.getFiatStakingBalance(blockchainId: String): BigDecimal {
val yieldBalance = yieldBalance as? YieldBalance.Data
val stakingBalance = yieldBalance?.getTotalWithRewardsStakingBalance(blockchainId).orZero()
return fiatRate?.times(stakingBalance).orZero()
}
private inline fun TotalFiatBalance.fold(
ifLoaded: (TotalFiatBalance.Loaded) -> TotalFiatBalance,
ifNot: () -> TotalFiatBalance,
): TotalFiatBalance {
return if (this is TotalFiatBalance.Loaded) {
ifLoaded(this)
} else {
ifNot()
}
}
private enum class ComputationState {
LOADING, NON_COMPUTABLE, COMPUTABLE;
companion object {
fun resolve(statuses: NonEmptyList<CryptoCurrencyStatus>): ComputationState {
for (status in statuses) {
when (status.value) {
CryptoCurrencyStatus.Loading -> {
return LOADING
}
is CryptoCurrencyStatus.NoQuote,
is CryptoCurrencyStatus.MissedDerivation,
-> {
return NON_COMPUTABLE
}
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
-> {
val blockchainId = status.currency.network.rawId
if (!BlockchainUtils.isIncludeToBalanceOnError(blockchainId)) {
return NON_COMPUTABLE
}
}
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.Custom,
is CryptoCurrencyStatus.NoAccount,
-> continue
}
}
return COMPUTABLE
}
}
}
}

View file

@ -80,7 +80,6 @@ internal object MockTokenLists {
sortedBy = TokensSortType.NONE,
totalFiatBalance = TotalFiatBalance.Loaded(
amount = tokens.sumOf { it.value.fiatAmount ?: BigDecimal.ZERO },
isAllAmountsSummarized = true,
source = StatusSource.ACTUAL,
),
)
@ -97,7 +96,6 @@ internal object MockTokenLists {
amount = groups
.flatMap { it.currencies as NonEmptyList<CryptoCurrencyStatus> }
.sumOf { it.value.fiatAmount ?: BigDecimal.ZERO },
isAllAmountsSummarized = true,
source = StatusSource.ACTUAL,
),
)

View file

@ -0,0 +1,506 @@
package com.tangem.domain.tokens.operations
import arrow.core.nonEmptyListOf
import com.google.common.truth.Truth
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.staking.BalanceItem
import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.YieldBalance
import com.tangem.domain.models.staking.YieldBalanceItem
import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TotalFiatBalanceCalculatorTest {
private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
private val binance = cryptoCurrencyFactory.createCoin(Blockchain.Binance)
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class LoadingOrNonComputable {
@Test
fun `one token is Loading, total is Loading`() {
// Arrange
val statuses = nonEmptyListOf(
createLoading(currency = cryptoCurrencyFactory.ethereum),
createNoQuote(currency = cryptoCurrencyFactory.stellar),
createMissedDerivation(currency = cryptoCurrencyFactory.chia),
)
// Act
val actual = TotalFiatBalanceCalculator.calculate(statuses)
// Assert
val expected = TotalFiatBalance.Loading
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `one token is NoQuote, total is Failed`() {
// Arrange
val statuses = nonEmptyListOf(
createNoQuote(currency = cryptoCurrencyFactory.stellar),
createUnreachable(currency = cryptoCurrencyFactory.chia),
)
// Act
val actual = TotalFiatBalanceCalculator.calculate(statuses)
// Assert
val expected = TotalFiatBalance.Failed
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `one token is MissedDerivation, total is Failed`() {
// Arrange
val statuses = nonEmptyListOf(
createNoQuote(currency = cryptoCurrencyFactory.stellar),
createUnreachable(currency = cryptoCurrencyFactory.chia),
)
// Act
val actual = TotalFiatBalanceCalculator.calculate(statuses)
// Assert
val expected = TotalFiatBalance.Failed
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `one token is Unreachable and isIncludeToBalanceOnError is FALSE, total is Failed`() {
// Arrange
val statuses = nonEmptyListOf(
createUnreachable(currency = cryptoCurrencyFactory.stellar),
createNoAccount(currency = cryptoCurrencyFactory.chia),
)
// Act
val actual = TotalFiatBalanceCalculator.calculate(statuses)
// Assert
val expected = TotalFiatBalance.Failed
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `one token is NoAmount and isIncludeToBalanceOnError is FALSE, total is Failed`() {
// Arrange
val statuses = nonEmptyListOf(
createNoAmount(currency = cryptoCurrencyFactory.stellar),
createNoAccount(currency = cryptoCurrencyFactory.chia),
)
// Act
val actual = TotalFiatBalanceCalculator.calculate(statuses)
// Assert
val expected = TotalFiatBalance.Failed
Truth.assertThat(actual).isEqualTo(expected)
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class Computable {
@Test
fun `Unreachable token isIncludeToBalanceOnError, total is Loaded`() {
// Arrange
val statuses = nonEmptyListOf(createUnreachable(currency = binance))
// Act
val actual = TotalFiatBalanceCalculator.calculate(statuses)
// Assert
val expected = TotalFiatBalance.Loaded(
amount = BigDecimal.ZERO,
source = StatusSource.ACTUAL,
)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `NoAmount token isIncludeToBalanceOnError, total is Loaded`() {
// Arrange
val statuses = nonEmptyListOf(createNoAmount(currency = binance))
// Act
val actual = TotalFiatBalanceCalculator.calculate(statuses)
// Assert
val expected = TotalFiatBalance.Loaded(
amount = BigDecimal.ZERO,
source = StatusSource.ACTUAL,
)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `all tokens are NoAccount, total is Loaded`() {
// Arrange
val statuses = nonEmptyListOf(
createNoAccount(currency = cryptoCurrencyFactory.ethereum),
createNoAccount(currency = cryptoCurrencyFactory.stellar),
createNoAccount(currency = cryptoCurrencyFactory.chia),
)
// Act
val actual = TotalFiatBalanceCalculator.calculate(statuses)
// Assert
val expected = TotalFiatBalance.Loaded(
amount = BigDecimal.ZERO,
source = StatusSource.ACTUAL,
)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `all tokens are Custom, total is Loaded`() {
// Arrange
val statuses = nonEmptyListOf(
/**
* Balance: 10
* - fiat: 10
* - staking: 0
*/
createCustom(
currency = cryptoCurrencyFactory.ethereum,
fiatAmount = BigDecimal.TEN,
),
/**
* Balance: 20
* - fiat: 0
* - staking: 20 (REWARDS)
*/
createCustom(
currency = cryptoCurrencyFactory.cardano,
fiatAmount = BigDecimal.ZERO,
yieldBalance = createYieldBalance(
amount = BigDecimal(20),
// It is important to use `BalanceType.REWARDS` because Cardano should not include the full
// staking balance. See `getTotalWithRewardsStakingBalance`.
balanceType = BalanceType.REWARDS,
),
),
/**
* Balance: 10
* - fiat: 1
* - staking: 9 (STAKED)
*/
createCustom(
currency = cryptoCurrencyFactory.stellar,
fiatAmount = BigDecimal.ONE,
yieldBalance = createYieldBalance(
amount = BigDecimal(9),
balanceType = BalanceType.STAKED,
),
),
)
// Act
val actual = TotalFiatBalanceCalculator.calculate(statuses)
// Assert
val expected = TotalFiatBalance.Loaded(
amount = BigDecimal(40),
source = StatusSource.ACTUAL,
)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `all tokens are Loaded, total is Loaded`() {
// Arrange
val statuses = nonEmptyListOf(
/**
* Balance: 10
* - fiat: 10
* - staking: 0
*/
createLoaded(
currency = cryptoCurrencyFactory.ethereum,
fiatAmount = BigDecimal.TEN,
),
/**
* Balance: 20
* - fiat: 0
* - staking: 20 (REWARDS)
*/
createLoaded(
currency = cryptoCurrencyFactory.cardano,
fiatAmount = BigDecimal.ZERO,
yieldBalance = createYieldBalance(
amount = BigDecimal(20),
// It is important to use `BalanceType.REWARDS` because Cardano should not include the full
// staking balance. See `getTotalWithRewardsStakingBalance`.
balanceType = BalanceType.REWARDS,
),
),
/**
* Balance: 10
* - fiat: 1
* - staking: 9 (STAKED)
*/
createLoaded(
currency = cryptoCurrencyFactory.stellar,
fiatAmount = BigDecimal.ONE,
yieldBalance = createYieldBalance(
amount = BigDecimal(9),
balanceType = BalanceType.STAKED,
),
),
)
// Act
val actual = TotalFiatBalanceCalculator.calculate(statuses)
// Assert
val expected = TotalFiatBalance.Loaded(
amount = BigDecimal(40),
source = StatusSource.ACTUAL,
)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `tokens contain all types of computable statuses, total is Loaded`() {
// Arrange
val statuses = nonEmptyListOf(
createNoAccount(currency = cryptoCurrencyFactory.ethereum), // 0
createNoAmount(currency = binance), // 0
createUnreachable(currency = binance), // 0
createCustom(
// 1
currency = cryptoCurrencyFactory.cardano,
fiatAmount = BigDecimal.ONE,
),
createLoaded(
// 10
currency = cryptoCurrencyFactory.stellar,
fiatAmount = BigDecimal.TEN,
),
)
// Act
val actual = TotalFiatBalanceCalculator.calculate(statuses)
// Assert
val expected = TotalFiatBalance.Loaded(
amount = BigDecimal(11),
source = StatusSource.ACTUAL,
)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `StatusSource is Actual is all tokens are Actual`() {
// Arrange
val statuses = nonEmptyListOf(
createNoAccount(currency = cryptoCurrencyFactory.ethereum),
createNoAmount(currency = binance),
createUnreachable(currency = binance),
createCustom(
currency = cryptoCurrencyFactory.cardano,
fiatAmount = BigDecimal.ONE,
),
createLoaded(
currency = cryptoCurrencyFactory.stellar,
fiatAmount = BigDecimal.TEN,
),
)
// Act
val actual = (TotalFiatBalanceCalculator.calculate(statuses) as TotalFiatBalance.Loaded).source
// Assert
val expected = StatusSource.ACTUAL
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `StatusSource is Cache is any token is Cache`() {
// Arrange
val statuses = nonEmptyListOf(
createNoAccount(currency = cryptoCurrencyFactory.ethereum),
createNoAmount(currency = binance),
createLoaded(
currency = cryptoCurrencyFactory.cardano,
fiatAmount = BigDecimal.ONE,
source = StatusSource.CACHE,
),
)
// Act
val actual = (TotalFiatBalanceCalculator.calculate(statuses) as TotalFiatBalance.Loaded).source
// Assert
val expected = StatusSource.CACHE
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `StatusSource is ONLY_CACHE is any token is ONLY_CACHE`() {
// Arrange
val statuses = nonEmptyListOf(
createNoAccount(currency = cryptoCurrencyFactory.ethereum),
createNoAmount(currency = binance),
createLoaded(
currency = cryptoCurrencyFactory.cardano,
fiatAmount = BigDecimal.ONE,
source = StatusSource.ONLY_CACHE,
),
)
// Act
val actual = (TotalFiatBalanceCalculator.calculate(statuses) 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,
value = CryptoCurrencyStatus.Loading,
)
}
private fun createNoQuote(currency: CryptoCurrency): CryptoCurrencyStatus {
return CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.NoQuote(
amount = BigDecimal.ONE,
yieldBalance = null,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = createNetworkAddress(),
sources = CryptoCurrencyStatus.Sources(),
),
)
}
private fun createMissedDerivation(currency: CryptoCurrency): CryptoCurrencyStatus {
return CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.MissedDerivation(priceChange = null, fiatRate = null),
)
}
private fun createUnreachable(currency: CryptoCurrency): CryptoCurrencyStatus {
return CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Unreachable(priceChange = null, fiatRate = null, networkAddress = null),
)
}
private fun createNoAmount(currency: CryptoCurrency): CryptoCurrencyStatus {
return CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.NoAmount(priceChange = null, fiatRate = null),
)
}
private fun createNoAccount(currency: CryptoCurrency): CryptoCurrencyStatus {
return CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.NoAccount(
priceChange = null,
amountToCreateAccount = BigDecimal.ONE,
fiatAmount = null,
fiatRate = null,
networkAddress = createNetworkAddress(),
sources = CryptoCurrencyStatus.Sources(),
),
)
}
private fun createCustom(
currency: CryptoCurrency,
fiatAmount: BigDecimal?,
yieldBalance: YieldBalance? = null,
): CryptoCurrencyStatus {
return CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Custom(
amount = BigDecimal.ONE,
fiatAmount = fiatAmount,
fiatRate = BigDecimal.ONE,
priceChange = BigDecimal.ZERO,
yieldBalance = yieldBalance,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = createNetworkAddress(),
sources = CryptoCurrencyStatus.Sources(),
),
)
}
private fun createLoaded(
currency: CryptoCurrency,
fiatAmount: BigDecimal,
yieldBalance: YieldBalance? = null,
source: StatusSource = StatusSource.ACTUAL,
): CryptoCurrencyStatus {
return CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Loaded(
amount = BigDecimal.ONE,
fiatAmount = fiatAmount,
fiatRate = BigDecimal.ONE,
priceChange = BigDecimal.ZERO,
yieldBalance = yieldBalance,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = createNetworkAddress(),
sources = CryptoCurrencyStatus.Sources(source, source, source),
),
)
}
private fun createYieldBalance(amount: BigDecimal, balanceType: BalanceType): YieldBalance.Data {
return YieldBalance.Data(
stakingId = mockk(),
source = StatusSource.ACTUAL,
balance = YieldBalanceItem(
items = listOf(
mockk<BalanceItem> {
every { this@mockk.amount } returns amount
every { this@mockk.type } returns balanceType
},
),
integrationId = "",
),
)
}
private fun createNetworkAddress(): NetworkAddress.Single {
return NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(
value = "0x1",
type = NetworkAddress.Address.Type.Primary,
),
)
}
}