Updated on 2026-08-14
This commit is contained in:
parent
5b25eb6312
commit
9f69ba143a
9 changed files with 708 additions and 134 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -70,7 +70,6 @@ sealed interface TokenList {
|
|||
|
||||
override val totalFiatBalance: TotalFiatBalance = TotalFiatBalance.Loaded(
|
||||
amount = SerializedBigDecimal.ZERO,
|
||||
isAllAmountsSummarized = true,
|
||||
source = StatusSource.ACTUAL,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue