Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-01 13:21:26 +05:00
parent 20dc8c9d90
commit fb34b46df6
54 changed files with 909 additions and 208 deletions

View file

@ -55,8 +55,14 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
): GetTokenListUseCase {
return GetTokenListUseCase(currenciesRepository, quotesRepository, networksRepository)
return GetTokenListUseCase(
currenciesRepository,
quotesRepository,
networksRepository,
stakingRepository,
)
}
@Provides
@ -65,8 +71,9 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
): GetCardTokensListUseCase {
return GetCardTokensListUseCase(currenciesRepository, quotesRepository, networksRepository)
return GetCardTokensListUseCase(currenciesRepository, quotesRepository, networksRepository, stakingRepository)
}
@Provides
@ -84,9 +91,16 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
dispatchers: CoroutineDispatcherProvider,
): GetCurrencyStatusUpdatesUseCase {
return GetCurrencyStatusUpdatesUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers)
return GetCurrencyStatusUpdatesUseCase(
currenciesRepository,
quotesRepository,
networksRepository,
stakingRepository,
dispatchers,
)
}
@Provides
@ -101,6 +115,7 @@ internal object TokensDomainModule {
currencyChecksRepository: CurrencyChecksRepository,
showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
promoRepository: PromoRepository,
stakingRepository: StakingRepository,
dispatchers: CoroutineDispatcherProvider,
): GetCurrencyWarningsUseCase {
return GetCurrencyWarningsUseCase(
@ -113,6 +128,7 @@ internal object TokensDomainModule {
swapRepository = swapRepository,
showSwapPromoTokenUseCase = showSwapPromoTokenUseCase,
promoRepository = promoRepository,
stakingRepository = stakingRepository,
dispatchers = dispatchers,
)
}
@ -123,12 +139,14 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
dispatchers: CoroutineDispatcherProvider,
): GetPrimaryCurrencyStatusUpdatesUseCase {
return GetPrimaryCurrencyStatusUpdatesUseCase(
currenciesRepository,
quotesRepository,
networksRepository,
stakingRepository,
dispatchers,
)
}
@ -149,8 +167,9 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
): FetchCardTokenListUseCase {
return FetchCardTokenListUseCase(currenciesRepository, networksRepository, quotesRepository)
return FetchCardTokenListUseCase(currenciesRepository, networksRepository, quotesRepository, stakingRepository)
}
@Provides
@ -212,12 +231,14 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
dispatchers: CoroutineDispatcherProvider,
): GetNetworkCoinStatusUseCase {
return GetNetworkCoinStatusUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
dispatchers = dispatchers,
)
}
@ -228,12 +249,14 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
dispatchers: CoroutineDispatcherProvider,
): GetFeePaidCryptoCurrencyStatusSyncUseCase {
return GetFeePaidCryptoCurrencyStatusSyncUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
dispatchers = dispatchers,
)
}
@ -366,11 +389,13 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
): GetWalletTotalBalanceUseCase {
return GetWalletTotalBalanceUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
)
}
}

View file

@ -5,7 +5,7 @@ import com.squareup.moshi.Json
data class YieldBalanceRequestBody(
@Json(name = "addresses") val addresses: Address,
@Json(name = "args") val args: YieldBalanceRequestArgs,
@Json(name = "integrationId") val integrationId: String? = null,
@Json(name = "integrationId") val integrationId: String,
) {
data class YieldBalanceRequestArgs(

View file

@ -0,0 +1,10 @@
package com.tangem.utils.extensions
import java.math.BigDecimal
/**
* Converts `BigDecimal?` to `BigDecimal`
*
* If `BigDecimal?` is `null`, returns `BigDecimal.ZERO`
*/
fun BigDecimal?.orZero(): BigDecimal = this ?: BigDecimal.ZERO

View file

@ -24,6 +24,8 @@ dependencies {
implementation(projects.domain.staking)
implementation(projects.domain.wallets.models)
/** Feature Api modules */
implementation(projects.features.staking.api)
// region DI
implementation(deps.hilt.android)

View file

@ -1,5 +1,6 @@
package com.tangem.data.staking
import arrow.core.raise.catch
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.data.staking.converters.StakingNetworkTypeConverter
@ -29,27 +30,33 @@ import com.tangem.data.staking.converters.*
import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.datasource.local.token.StakingBalanceStore
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.lce.lceFlow
import com.tangem.domain.staking.model.*
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.toFormattedString
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.cancellable
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
@Suppress("LargeClass")
internal class DefaultStakingRepository(
private val stakeKitApi: StakeKitApi,
private val stakingYieldsStore: StakingYieldsStore,
private val stakingBalanceStore: StakingBalanceStore,
private val cacheRegistry: CacheRegistry,
private val dispatchers: CoroutineDispatcherProvider,
private val stakingFeatureToggle: StakingFeatureToggles,
) : StakingRepository {
private val stakingNetworkTypeConverter = StakingNetworkTypeConverter()
@ -83,15 +90,24 @@ internal class DefaultStakingRepository(
private val yieldBalanceListConverter = YieldBalanceListConverter()
private val isYieldBalanceFetching = MutableStateFlow(
value = emptyMap<UserWalletId, Boolean>(),
)
override fun isStakingSupported(currencyId: String): Boolean {
return integrationIds.contains(currencyId)
return integrationIdMap.containsKey(currencyId)
}
override suspend fun fetchEnabledYields() {
override suspend fun fetchEnabledYields(refresh: Boolean) {
withContext(dispatchers.io) {
val stakingTokensWithYields = stakeKitApi.getMultipleYields().getOrThrow()
stakingYieldsStore.store(stakingTokensWithYields.data)
cacheRegistry.invokeOnExpire(
key = YIELDS_STORE_KEY,
skipCache = refresh,
block = {
val stakingTokensWithYields = stakeKitApi.getMultipleYields().getOrThrow()
stakingYieldsStore.store(stakingTokensWithYields.data)
},
)
}
}
@ -182,24 +198,32 @@ internal class DefaultStakingRepository(
override suspend fun fetchSingleYieldBalance(
userWalletId: UserWalletId,
address: String,
integrationId: String,
address: CryptoCurrencyAddress,
refresh: Boolean,
) = withContext(dispatchers.io) {
if (!stakingFeatureToggle.isStakingEnabled) return@withContext
val cryptoCurrency = address.cryptoCurrency
val rawCurrencyId =
cryptoCurrency.id.rawCurrencyId ?: error("Staking custom tokens is not available")
val integrationId = integrationIdMap[rawCurrencyId] ?: return@withContext
cacheRegistry.invokeOnExpire(
key = getYieldBalancesKey(userWalletId),
skipCache = refresh,
block = {
val requestBody = getBalanceRequestData(address.address, integrationId)
val result = stakeKitApi.getSingleYieldBalance(
integrationId = integrationId,
body = getBalanceRequestData(address, integrationId),
integrationId = requestBody.integrationId,
body = requestBody,
).getOrThrow()
stakingBalanceStore.store(
integrationId,
requestBody.integrationId,
YieldBalanceWrapperDTO(
balances = result,
integrationId = integrationId,
integrationId = requestBody.integrationId,
),
)
},
@ -208,70 +232,157 @@ internal class DefaultStakingRepository(
override fun getSingleYieldBalanceFlow(
userWalletId: UserWalletId,
address: String,
integrationId: String,
address: CryptoCurrencyAddress,
): Flow<YieldBalance> = channelFlow {
launch(dispatchers.io) {
stakingBalanceStore.get(integrationId)
.collectLatest {
send(
yieldBalanceConverter.convert(
YieldBalanceConverter.Data(
balance = it,
integrationId = integrationId,
if (!stakingFeatureToggle.isStakingEnabled) {
send(YieldBalance.Empty)
} else {
launch(dispatchers.io) {
val integrationId = integrationIdMap[address.cryptoCurrency.id.rawCurrencyId]
?: error("Could not get integrationId")
stakingBalanceStore.get(integrationId)
.collectLatest {
send(
yieldBalanceConverter.convert(
YieldBalanceConverter.Data(
balance = it,
integrationId = integrationId,
),
),
),
)
}
}
)
}
}
withContext(dispatchers.io) {
fetchSingleYieldBalance(
userWalletId,
address,
integrationId,
)
withContext(dispatchers.io) {
fetchSingleYieldBalance(
userWalletId,
address,
)
}
}
}.cancellable()
override suspend fun getSingleYieldBalanceSync(
userWalletId: UserWalletId,
address: CryptoCurrencyAddress,
): YieldBalance = withContext(dispatchers.io) {
if (!stakingFeatureToggle.isStakingEnabled) {
YieldBalance.Empty
} else {
fetchSingleYieldBalance(userWalletId, address)
val integrationId = integrationIdMap[address.cryptoCurrency.id.rawCurrencyId]
?: error("Could not get integrationId")
val result = stakingBalanceStore.getSyncOrNull(integrationId) ?: return@withContext YieldBalance.Error
yieldBalanceConverter.convert(
YieldBalanceConverter.Data(
balance = result,
integrationId = integrationId,
),
)
}
}
override suspend fun fetchMultiYieldBalance(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
integrationId: String,
refresh: Boolean,
) = withContext(dispatchers.io) {
cacheRegistry.invokeOnExpire(
key = getYieldBalancesKey(userWalletId),
skipCache = refresh,
block = {
val result = stakeKitApi.getMultipleYieldBalances(
addresses.map { getBalanceRequestData(it.address, integrationId) },
).getOrThrow()
if (!stakingFeatureToggle.isStakingEnabled) return@withContext
try {
isYieldBalanceFetching.update {
it + (userWalletId to true)
}
cacheRegistry.invokeOnExpire(
key = getYieldBalancesKey(userWalletId),
skipCache = refresh,
block = {
val result = stakeKitApi.getMultipleYieldBalances(
addresses
.mapNotNull { networkAddress ->
val cryptoCurrency = networkAddress.cryptoCurrency
val rawCurrencyId = cryptoCurrency.id.rawCurrencyId ?: error("Currency raw id is null")
val integrationId = integrationIdMap[rawCurrencyId]
stakingBalanceStore.store(result)
},
)
if (integrationId != null) {
networkAddress.address to integrationId
} else {
null
}
}
.distinct()
.map { getBalanceRequestData(it.first, it.second) },
).getOrThrow()
stakingBalanceStore.store(result)
},
)
} finally {
isYieldBalanceFetching.update {
it - userWalletId
}
}
}
override fun getMultiYieldBalanceFlow(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
integrationId: String,
): Flow<YieldBalanceList> = channelFlow {
launch(dispatchers.io) {
stakingBalanceStore.get()
.collectLatest { send(yieldBalanceListConverter.convert(it)) }
}
if (!stakingFeatureToggle.isStakingEnabled) {
send(YieldBalanceList.Empty)
} else {
launch(dispatchers.io) {
stakingBalanceStore.get()
.collectLatest { send(yieldBalanceListConverter.convert(it)) }
}
withContext(dispatchers.io) {
fetchMultiYieldBalance(
userWalletId,
addresses,
integrationId,
)
withContext(dispatchers.io) {
fetchMultiYieldBalance(
userWalletId,
addresses,
)
}
}
}.cancellable()
override fun getMultiYieldBalanceLce(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
): LceFlow<Throwable, YieldBalanceList> = lceFlow {
if (!stakingFeatureToggle.isStakingEnabled) {
send(YieldBalanceList.Empty)
} else {
launch(dispatchers.io) {
combine(
stakingBalanceStore.get(),
isYieldBalanceFetching.map { it.getOrElse(userWalletId) { false } },
) { result, isFetching ->
val balances = yieldBalanceListConverter.convert(result)
send(balances, isStillLoading = isFetching)
}.collect()
}
withContext(dispatchers.io) {
catch(
block = { fetchMultiYieldBalance(userWalletId, addresses, refresh = false) },
catch = { raise(it) },
)
}
}
}
override suspend fun getMultiYieldBalanceSync(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
): YieldBalanceList = withContext(dispatchers.io) {
if (!stakingFeatureToggle.isStakingEnabled) {
YieldBalanceList.Empty
} else {
fetchMultiYieldBalance(userWalletId, addresses)
val result = stakingBalanceStore.getSyncOrNull() ?: return@withContext YieldBalanceList.Error
yieldBalanceListConverter.convert(result)
}
}
private fun findPrefetchedYield(yields: List<Yield>, currencyId: String, symbol: String): Yield? {
return yields.find { it.token.coinGeckoId == currencyId && it.token.symbol == symbol }
}
@ -297,19 +408,33 @@ internal class DefaultStakingRepository(
private fun getYieldBalancesKey(userWalletId: UserWalletId) = "yield_balance_${userWalletId.stringValue}"
companion object {
private val integrationIds = setOf(
Blockchain.Solana.toCoinId(),
Blockchain.Cosmos.toCoinId(),
Blockchain.Polkadot.toCoinId(),
Blockchain.Polygon.toCoinId(),
Blockchain.Avalanche.toCoinId(),
Blockchain.Tron.toCoinId(),
Blockchain.Cronos.toCoinId(),
Blockchain.Binance.toCoinId(),
Blockchain.Kava.toCoinId(),
Blockchain.Near.toCoinId(),
Blockchain.Tezos.toCoinId(),
private companion object {
const val YIELDS_STORE_KEY = "yields"
const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking"
const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking"
const val POLKADOT_INTEGRATION_ID = "polkadot-dot-validator-staking"
const val ETHEREUM_INTEGRATION_ID = "ethereum-matic-native-staking"
const val AVALANCHE_INTEGRATION_ID = "avalanche-avax-native-staking"
const val TRON_INTEGRATION_ID = "tron-trx-native-staking"
const val CRONOS_INTEGRATION_ID = "cronos-cro-native-staking"
const val BINANCE_INTEGRATION_ID = "binance-bnb-native-staking"
const val KAVA_INTEGRATION_ID = "kava-kava-native-staking"
const val NEAR_INTEGRATION_ID = "near-near-native-staking"
const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking"
val integrationIdMap = mapOf(
Blockchain.Solana.toCoinId() to SOLANA_INTEGRATION_ID,
Blockchain.Cosmos.toCoinId() to COSMOS_INTEGRATION_ID,
Blockchain.Polkadot.toCoinId() to POLKADOT_INTEGRATION_ID,
Blockchain.Polygon.toCoinId() to ETHEREUM_INTEGRATION_ID,
Blockchain.Avalanche.toCoinId() to AVALANCHE_INTEGRATION_ID,
Blockchain.Tron.toCoinId() to TRON_INTEGRATION_ID,
Blockchain.Cronos.toCoinId() to CRONOS_INTEGRATION_ID,
Blockchain.Binance.toCoinId() to BINANCE_INTEGRATION_ID,
Blockchain.Kava.toCoinId() to KAVA_INTEGRATION_ID,
Blockchain.Near.toCoinId() to NEAR_INTEGRATION_ID,
Blockchain.Tezos.toCoinId() to TEZOS_INTEGRATION_ID,
)
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.local.token.StakingBalanceStore
import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -24,6 +25,7 @@ internal object StakingDataModule {
stakingTokenStore: StakingYieldsStore,
stakingBalanceStore: StakingBalanceStore,
dispatchers: CoroutineDispatcherProvider,
stakingFeatureToggle: StakingFeatureToggles,
cacheRegistry: CacheRegistry,
): StakingRepository {
return DefaultStakingRepository(
@ -32,6 +34,7 @@ internal object StakingDataModule {
stakingBalanceStore = stakingBalanceStore,
dispatchers = dispatchers,
cacheRegistry = cacheRegistry,
stakingFeatureToggle = stakingFeatureToggle,
)
}
}

View file

@ -124,6 +124,60 @@ internal class DefaultNetworksRepository(
}
}
override suspend fun getNetworkAddress(
userWalletId: UserWalletId,
currency: CryptoCurrency,
): CryptoCurrencyAddress = withContext(dispatchers.io) {
CryptoCurrencyAddress(
cryptoCurrency = currency,
address = walletManagersFacade.getAddresses(userWalletId, currency.network)
.firstOrNull { it.type == AddressType.Default }
?.value.orEmpty(),
)
}
override fun getNetworkAddressFlow(
userWalletId: UserWalletId,
currency: CryptoCurrency,
): Flow<CryptoCurrencyAddress> = channelFlow {
launch(dispatchers.io) {
send(getNetworkAddress(userWalletId, currency))
}
}
override suspend fun getNetworkAddresses(userWalletId: UserWalletId): List<CryptoCurrencyAddress> =
withContext(dispatchers.io) {
// Get list of currencies matching [network]
val currencies = getCurrencies(userWalletId)
// There is no currencies matching given [networks] in [userWalletId]
if (currencies.toList().isEmpty()) return@withContext emptyList()
currencies.toList().map { currency ->
CryptoCurrencyAddress(
cryptoCurrency = currency,
address = walletManagersFacade.getAddresses(userWalletId, currency.network)
.firstOrNull { it.type == AddressType.Default }
?.value.orEmpty(),
)
}
}
override fun getNetworkAddressesFlow(
userWalletId: UserWalletId,
network: Network,
): Flow<List<CryptoCurrencyAddress>> = channelFlow {
launch(dispatchers.io) {
send(getNetworkAddresses(userWalletId, network))
}
}
override fun getNetworkAddressesFlow(userWalletId: UserWalletId): Flow<List<CryptoCurrencyAddress>> = channelFlow {
launch(dispatchers.io) {
send(getNetworkAddresses(userWalletId))
}
}
private suspend fun fetchNetworksStatusesIfCacheExpired(
userWalletId: UserWalletId,
networks: Set<Network>,

View file

@ -18,4 +18,6 @@ dependencies {
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.features.staking.api)
}

View file

@ -6,9 +6,23 @@ sealed class YieldBalance {
data class Data(
val balance: YieldBalanceItem,
) : YieldBalance()
) : YieldBalance() {
fun getTotalStakingBalance(): BigDecimal {
return balance.items
.filterNot { it.type == BalanceType.REWARDS }
.sumOf { it.amount * it.pricePerShare }
}
fun getRewardStakingBalance(): BigDecimal {
return balance.items
.filter { it.type == BalanceType.REWARDS }
.sumOf { it.amount * it.pricePerShare }
}
}
data object Empty : YieldBalance()
data object Error : YieldBalance()
}
data class YieldBalanceItem(

View file

@ -5,13 +5,15 @@ sealed class YieldBalanceList {
data class Data(
val balances: List<YieldBalance>,
) : YieldBalanceList() {
fun getBalance(rawCurrencyId: String?): YieldBalance? {
fun getBalance(rawCurrencyId: String?): YieldBalance {
return balances.firstOrNull { yield ->
(yield as? YieldBalance.Data)?.balance?.items
?.any { it.rawCurrencyId == rawCurrencyId } == true
}
} ?: YieldBalance.Error
}
}
data object Empty : YieldBalanceList()
data object Error : YieldBalanceList()
}

View file

@ -12,10 +12,10 @@ import com.tangem.domain.staking.repositories.StakingRepository
class FetchStakingTokensUseCase(
private val stakingRepository: StakingRepository,
) {
suspend operator fun invoke(): Either<Throwable, Unit> {
suspend operator fun invoke(isRefresh: Boolean = false): Either<Throwable, Unit> {
return either {
catch(
block = { stakingRepository.fetchEnabledYields() },
block = { stakingRepository.fetchEnabledYields(isRefresh) },
catch = { StakingTokensError.DataError(it) },
)
}

View file

@ -2,6 +2,7 @@ package com.tangem.domain.staking
import arrow.core.Either
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.wallets.models.UserWalletId
class FetchStakingYieldBalanceUseCase(
@ -10,14 +11,12 @@ class FetchStakingYieldBalanceUseCase(
suspend operator fun invoke(
userWalletId: UserWalletId,
address: String,
integrationId: String,
address: CryptoCurrencyAddress,
refresh: Boolean = false,
): Either<Throwable, Unit> = Either.catch {
stakingRepository.fetchSingleYieldBalance(
userWalletId = userWalletId,
address = address,
integrationId = integrationId,
refresh = refresh,
)
}

View file

@ -6,6 +6,7 @@ import arrow.core.right
import com.tangem.domain.core.utils.EitherFlow
import com.tangem.domain.staking.model.YieldBalance
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
@ -16,13 +17,11 @@ class GetStakingYieldBalanceUseCase(
operator fun invoke(
userWalletId: UserWalletId,
address: String,
integrationId: String,
address: CryptoCurrencyAddress,
): EitherFlow<Throwable, YieldBalance> {
return stakingRepository.getSingleYieldBalanceFlow(
userWalletId = userWalletId,
address = address,
integrationId = integrationId,
).map<YieldBalance, Either<Throwable, YieldBalance>> { it.right() }
.catch { emit(it.left()) }
}

View file

@ -7,6 +7,7 @@ import com.tangem.domain.staking.model.Yield
import com.tangem.domain.staking.model.action.EnterAction
import com.tangem.domain.staking.model.transaction.StakingTransaction
import java.math.BigDecimal
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.staking.model.*
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
@ -17,7 +18,7 @@ interface StakingRepository {
fun isStakingSupported(currencyId: String): Boolean
suspend fun fetchEnabledYields()
suspend fun fetchEnabledYields(refresh: Boolean)
suspend fun getEntryInfo(integrationId: String): StakingEntryInfo
@ -30,30 +31,35 @@ interface StakingRepository {
suspend fun fetchSingleYieldBalance(
userWalletId: UserWalletId,
address: String,
integrationId: String,
address: CryptoCurrencyAddress,
refresh: Boolean = false,
)
fun getSingleYieldBalanceFlow(
userWalletId: UserWalletId,
address: String,
integrationId: String,
): Flow<YieldBalance>
fun getSingleYieldBalanceFlow(userWalletId: UserWalletId, address: CryptoCurrencyAddress): Flow<YieldBalance>
suspend fun getSingleYieldBalanceSync(userWalletId: UserWalletId, address: CryptoCurrencyAddress): YieldBalance
suspend fun fetchMultiYieldBalance(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
integrationId: String,
refresh: Boolean = false,
)
fun getMultiYieldBalanceFlow(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
integrationId: String,
): Flow<YieldBalanceList>
fun getMultiYieldBalanceLce(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
): LceFlow<Throwable, YieldBalanceList>
suspend fun getMultiYieldBalanceSync(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
): YieldBalanceList
suspend fun createEnterAction(
integrationId: String,
amount: BigDecimal,

View file

@ -24,7 +24,10 @@ dependencies {
implementation(projects.domain.settings)
implementation(projects.features.swap.domain.api)
implementation(projects.features.swap.domain.models)
/** Project - Api */
implementation(projects.features.send.api)
implementation(projects.features.staking.api)
/** Project - Other */
implementation(projects.core.utils)

View file

@ -10,12 +10,20 @@ android {
}
dependencies {
implementation(deps.kotlin.serialization)
implementation(projects.domain.txhistory.models)
/** Project - Core */
implementation(projects.core.analytics.models)
/** Project - Domain */
implementation(projects.domain.txhistory.models)
implementation(projects.domain.staking.models)
/** SDK dependencies */
implementation(deps.tangem.blockchain) {
exclude(module = "joda-time")
}
/** Other dependencies */
implementation(deps.kotlin.serialization)
implementation(deps.jodatime)
implementation(deps.timber)
}

View file

@ -1,5 +1,6 @@
package com.tangem.domain.tokens.model
import com.tangem.domain.staking.model.YieldBalance
import com.tangem.domain.txhistory.models.TxHistoryItem
import java.math.BigDecimal
@ -45,6 +46,9 @@ data class CryptoCurrencyStatus(
/** The network address */
open val networkAddress: NetworkAddress? = null
/** Staking yield balance */
open val yieldBalance: YieldBalance? = null
}
/** Represents the Loading state of a cryptocurrency, typically while fetching its details. */
@ -107,6 +111,7 @@ data class CryptoCurrencyStatus(
override val fiatAmount: BigDecimal,
override val fiatRate: BigDecimal,
override val priceChange: BigDecimal,
override val yieldBalance: YieldBalance?,
override val hasCurrentNetworkTransactions: Boolean,
override val pendingTransactions: Set<TxHistoryItem>,
override val networkAddress: NetworkAddress,
@ -128,6 +133,7 @@ data class CryptoCurrencyStatus(
override val fiatAmount: BigDecimal?,
override val fiatRate: BigDecimal?,
override val priceChange: BigDecimal?,
override val yieldBalance: YieldBalance?,
override val hasCurrentNetworkTransactions: Boolean,
override val pendingTransactions: Set<TxHistoryItem>,
override val networkAddress: NetworkAddress,
@ -143,6 +149,7 @@ data class CryptoCurrencyStatus(
*/
data class NoQuote(
override val amount: BigDecimal,
override val yieldBalance: YieldBalance?,
override val hasCurrentNetworkTransactions: Boolean,
override val pendingTransactions: Set<TxHistoryItem>,
override val networkAddress: NetworkAddress,

View file

@ -4,6 +4,7 @@ import arrow.core.Either
import arrow.core.raise.Raise
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
@ -19,6 +20,7 @@ class FetchCardTokenListUseCase(
private val currenciesRepository: CurrenciesRepository,
private val networksRepository: NetworksRepository,
private val quotesRepository: QuotesRepository,
private val stakingRepository: StakingRepository,
) {
suspend operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = false): Either<TokenListError, Unit> {
@ -39,8 +41,13 @@ class FetchCardTokenListUseCase(
refresh = refresh,
)
}
awaitAll(fetchStatuses, fetchQuotes)
val yieldBalances = async {
fetchYieldBalances(
userWalletId = userWalletId,
refresh = refresh,
)
}
awaitAll(fetchStatuses, fetchQuotes, yieldBalances)
}
}
}
@ -69,4 +76,12 @@ class FetchCardTokenListUseCase(
catch = { /* Ignore error */ },
)
}
private suspend fun fetchYieldBalances(userWalletId: UserWalletId, refresh: Boolean) {
val networkAddresses = networksRepository.getNetworkAddresses(userWalletId)
catch(
block = { stakingRepository.fetchMultiYieldBalance(userWalletId, networkAddresses, refresh) },
catch = { /* Ignore error */ },
)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.domain.tokens
import arrow.core.left
import com.tangem.domain.core.utils.EitherFlow
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.error.mapper.mapToTokenListError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -21,6 +22,7 @@ class GetCardTokensListUseCase(
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val stakingRepository: StakingRepository,
) {
@OptIn(ExperimentalCoroutinesApi::class)
@ -43,6 +45,7 @@ class GetCardTokensListUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
)
return operations.getCardCurrenciesStatusesFlow()

View file

@ -43,6 +43,7 @@ class GetCryptoCurrencyActionsUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
userWalletId = userWallet.walletId,
)
val networkId = cryptoCurrencyStatus.currency.network.id

View file

@ -1,6 +1,7 @@
package com.tangem.domain.tokens
import arrow.core.Either
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.error.mapper.mapToCurrencyError
import com.tangem.domain.tokens.model.CryptoCurrency
@ -16,6 +17,7 @@ class GetCryptoCurrencyStatusSyncUseCase(
internal val currenciesRepository: CurrenciesRepository,
internal val quotesRepository: QuotesRepository,
internal val networksRepository: NetworksRepository,
internal val stakingRepository: StakingRepository,
internal val dispatchers: CoroutineDispatcherProvider,
) {
@ -29,6 +31,7 @@ class GetCryptoCurrencyStatusSyncUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
)
return operations.getCurrencyStatusSync(cryptoCurrencyId, isSingleWalletWithTokens)
@ -41,6 +44,7 @@ class GetCryptoCurrencyStatusSyncUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
)
return operations.getPrimaryCurrencyStatusSync()

View file

@ -1,6 +1,7 @@
package com.tangem.domain.tokens
import arrow.core.Either
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.error.mapper.mapToTokenListError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -15,6 +16,7 @@ class GetCryptoCurrencyStatusesSyncUseCase(
internal val currenciesRepository: CurrenciesRepository,
internal val quotesRepository: QuotesRepository,
internal val networksRepository: NetworksRepository,
internal val stakingRepository: StakingRepository,
internal val dispatchers: CoroutineDispatcherProvider,
) {
@ -24,6 +26,7 @@ class GetCryptoCurrencyStatusesSyncUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
)
return operations.getCurrenciesStatusesSync()

View file

@ -1,6 +1,7 @@
package com.tangem.domain.tokens
import arrow.core.Either
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.error.mapper.mapToCurrencyError
import com.tangem.domain.tokens.model.CryptoCurrency
@ -24,6 +25,7 @@ class GetCurrencyStatusUpdatesUseCase(
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val stakingRepository: StakingRepository,
private val dispatchers: CoroutineDispatcherProvider,
) {
@ -60,6 +62,7 @@ class GetCurrencyStatusUpdatesUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
userWalletId = userWalletId,
)

View file

@ -1,6 +1,7 @@
package com.tangem.domain.tokens
import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.tokens.model.warnings.HederaWarnings
@ -26,6 +27,7 @@ class GetCurrencyWarningsUseCase(
private val networksRepository: NetworksRepository,
private val swapRepository: SwapRepository,
private val marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
private val stakingRepository: StakingRepository,
private val promoRepository: PromoRepository,
private val showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
private val dispatchers: CoroutineDispatcherProvider,
@ -43,6 +45,7 @@ class GetCurrencyWarningsUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
userWalletId = userWalletId,
)
return combine(

View file

@ -2,6 +2,7 @@ package com.tangem.domain.tokens
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.FeePaidCurrency
@ -16,6 +17,7 @@ class GetFeePaidCryptoCurrencyStatusSyncUseCase(
internal val currenciesRepository: CurrenciesRepository,
internal val quotesRepository: QuotesRepository,
internal val networksRepository: NetworksRepository,
internal val stakingRepository: StakingRepository,
internal val dispatchers: CoroutineDispatcherProvider,
) {
@ -30,6 +32,7 @@ class GetFeePaidCryptoCurrencyStatusSyncUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
)
return either {

View file

@ -1,6 +1,7 @@
package com.tangem.domain.tokens
import arrow.core.Either
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.error.mapper.mapToCurrencyError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -17,6 +18,7 @@ class GetNetworkCoinStatusUseCase(
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val stakingRepository: StakingRepository,
private val dispatchers: CoroutineDispatcherProvider,
) {
@ -49,6 +51,7 @@ class GetNetworkCoinStatusUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
userWalletId = userWalletId,
)
val maybeCurrency = if (isSingleWalletWithTokens) {
@ -69,6 +72,7 @@ class GetNetworkCoinStatusUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
userWalletId = userWalletId,
)
val networkFlow = if (isSingleWalletWithTokens) {

View file

@ -1,6 +1,7 @@
package com.tangem.domain.tokens
import arrow.core.Either
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.error.mapper.mapToCurrencyError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -24,6 +25,7 @@ class GetPrimaryCurrencyStatusUpdatesUseCase(
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val stakingRepository: StakingRepository,
private val dispatchers: CoroutineDispatcherProvider,
) {
@ -46,6 +48,7 @@ class GetPrimaryCurrencyStatusUpdatesUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
userWalletId = userWalletId,
)

View file

@ -6,6 +6,7 @@ import com.tangem.domain.core.utils.EitherFlow
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.core.utils.toLce
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.error.mapper.mapToTokenListError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -26,6 +27,7 @@ class GetTokenListUseCase(
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val stakingRepository: StakingRepository,
) {
@OptIn(ExperimentalCoroutinesApi::class)
@ -35,6 +37,7 @@ class GetTokenListUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
)
return operations.getCurrenciesStatusesFlow().transformLatest { maybeTokens ->
@ -55,6 +58,7 @@ class GetTokenListUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
)
return operations.getCurrenciesStatuses(userWalletId).transformLatest { maybeCurrencies ->

View file

@ -6,6 +6,7 @@ import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.lce.lce
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TotalFiatBalance
@ -23,6 +24,7 @@ class GetWalletTotalBalanceUseCase(
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val stakingRepository: StakingRepository,
) {
suspend operator fun invoke(
@ -72,6 +74,7 @@ class GetWalletTotalBalanceUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
)
return operations.getCurrenciesStatuses(

View file

@ -6,6 +6,7 @@ import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
internal fun CurrenciesStatusesOperations.Error.mapToCurrencyError(): CurrencyStatusError {
return when (this) {
is CurrenciesStatusesOperations.Error.DataError -> CurrencyStatusError.DataError(this.cause)
is CurrenciesStatusesOperations.Error.EmptyYieldBalances,
is CurrenciesStatusesOperations.Error.EmptyNetworksStatuses,
is CurrenciesStatusesOperations.Error.EmptyQuotes,
is CurrenciesStatusesOperations.Error.EmptyCurrencies,

View file

@ -11,6 +11,7 @@ internal fun CurrenciesStatusesOperations.Error.mapToTokenListError(): TokenList
is CurrenciesStatusesOperations.Error.EmptyQuotes,
is CurrenciesStatusesOperations.Error.EmptyCurrencies,
is CurrenciesStatusesOperations.Error.UnableToCreateCurrencyStatus,
is CurrenciesStatusesOperations.Error.EmptyYieldBalances,
-> TokenListError.EmptyTokens
}
}

View file

@ -7,6 +7,9 @@ import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.lce.lce
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.staking.model.YieldBalance
import com.tangem.domain.staking.model.YieldBalanceList
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -20,6 +23,7 @@ internal class CurrenciesStatusesLceOperations(
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val stakingRepository: StakingRepository,
) {
fun getCurrenciesStatuses(
@ -65,11 +69,18 @@ internal class CurrenciesStatusesLceOperations(
val (networks, currenciesIds) = getIds(nonEmptyCurrencies)
val addresses = networksRepository.getNetworkAddresses(userWalletId)
combine(
getQuotes(currenciesIds),
getNetworksStatuses(userWalletId, networks),
) { maybeQuotes, maybeNetworksStatuses ->
val statuses = createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses)
getYieldBalances(userWalletId, addresses),
) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances ->
val statuses = createCurrenciesStatuses(
currencies = nonEmptyCurrencies,
maybeQuotes = maybeQuotes,
maybeNetworkStatuses = maybeNetworksStatuses,
maybeYieldBalances = maybeYieldBalances,
)
emit(statuses)
}.collect()
}
@ -84,9 +95,10 @@ internal class CurrenciesStatusesLceOperations(
lceLoading()
} else {
createCurrenciesStatuses(
nonEmptyCurrencies,
currencies = nonEmptyCurrencies,
maybeNetworkStatuses = null,
maybeQuotes = null,
maybeYieldBalances = null,
)
}
@ -113,6 +125,7 @@ internal class CurrenciesStatusesLceOperations(
currencies: NonEmptyList<CryptoCurrency>,
maybeQuotes: Either<TokenListError, Set<Quote>>?,
maybeNetworkStatuses: Lce<TokenListError, Set<NetworkStatus>>?,
maybeYieldBalances: Lce<TokenListError, YieldBalanceList>?,
): Lce<TokenListError, List<CryptoCurrencyStatus>> = lce {
isLoading.set(maybeNetworkStatuses == null)
@ -127,11 +140,20 @@ internal class CurrenciesStatusesLceOperations(
null
}
val yieldBalances = maybeYieldBalances?.getOrNull()
currencies.map { currency ->
val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId }
val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network }
val yieldBalance = (yieldBalances as? YieldBalanceList.Data)?.getBalance(currency.id.rawCurrencyId)
createCurrencyStatus(currency, quote, networkStatus, ignoreQuote = quotesRetrievingFailed)
createCurrencyStatus(
currency = currency,
quote = quote,
networkStatus = networkStatus,
yieldBalance = yieldBalance,
ignoreQuote = quotesRetrievingFailed,
)
}
}
@ -139,12 +161,14 @@ internal class CurrenciesStatusesLceOperations(
currency: CryptoCurrency,
quote: Quote?,
networkStatus: NetworkStatus?,
yieldBalance: YieldBalance?,
ignoreQuote: Boolean,
): CryptoCurrencyStatus {
val currencyStatusOperations = CurrencyStatusOperations(
currency = currency,
quote = quote,
networkStatus = networkStatus,
yieldBalance = yieldBalance,
ignoreQuote = ignoreQuote,
)
@ -167,6 +191,18 @@ internal class CurrenciesStatusesLceOperations(
}
}
private fun getYieldBalances(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
): LceFlow<TokenListError, YieldBalanceList> {
return stakingRepository.getMultiYieldBalanceLce(
userWalletId = userWalletId,
addresses = addresses,
).map { maybeBalances ->
maybeBalances.mapError { TokenListError.DataError(it) }
}
}
private fun getIds(currencies: List<CryptoCurrency>): Pair<NonEmptySet<Network>, NonEmptySet<CryptoCurrency.ID>> {
val currencyIdToNetworkId = currencies.associate { currency ->
currency.id to currency.network

View file

@ -3,6 +3,9 @@ package com.tangem.domain.tokens.operations
import arrow.core.*
import arrow.core.raise.*
import com.tangem.domain.core.utils.EitherFlow
import com.tangem.domain.staking.model.YieldBalance
import com.tangem.domain.staking.model.YieldBalanceList
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
@ -17,6 +20,7 @@ internal class CurrenciesStatusesOperations(
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val stakingRepository: StakingRepository,
private val userWalletId: UserWalletId,
) {
@ -42,6 +46,7 @@ internal class CurrenciesStatusesOperations(
currencies = nonEmptyCurrencies,
maybeNetworkStatuses = null,
maybeQuotes = null,
maybeYieldBalances = null,
)
emit(maybeLoadingCurrenciesStatuses)
@ -51,8 +56,14 @@ internal class CurrenciesStatusesOperations(
val currenciesFlow = combine(
getQuotes(currenciesIds),
getNetworksStatuses(networks),
) { maybeQuotes, maybeNetworksStatuses ->
createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses)
getYieldBalances(),
) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances ->
createCurrenciesStatuses(
currencies = nonEmptyCurrencies,
maybeQuotes = maybeQuotes,
maybeNetworkStatuses = maybeNetworksStatuses,
maybeYieldBalances = maybeYieldBalances,
)
}
emitAll(currenciesFlow)
@ -70,7 +81,9 @@ internal class CurrenciesStatusesOperations(
val quotes = quotesRepository.getQuotesSync(currenciesIds, false).right()
val networkStatuses =
networksRepository.getNetworkStatusesSync(userWalletId, networks, false).right()
return createCurrenciesStatuses(nonEmptyCurrencies, quotes, networkStatuses)
val yieldBalances = getYieldBalancesSync()
return createCurrenciesStatuses(nonEmptyCurrencies, quotes, networkStatuses, yieldBalances)
},
catch = { raise(Error.DataError(it)) },
)
@ -98,7 +111,9 @@ internal class CurrenciesStatusesOperations(
).firstOrNull {
it.network == currency.network
}.right()
return createCurrencyStatus(currency, quotes, networkStatuses)
val yieldBalances = getYieldBalanceSync(currency)
return createCurrencyStatus(currency, quotes, networkStatuses, yieldBalances)
},
catch = { raise(Error.DataError(it)) },
)
@ -142,8 +157,9 @@ internal class CurrenciesStatusesOperations(
},
catch = { Error.DataError(it).left() },
)
val yieldBalances = getYieldBalanceSync(currency)
return createCurrencyStatus(currency, quotes, networkStatus)
return createCurrencyStatus(currency, quotes, networkStatus, yieldBalances)
}
fun getCardCurrenciesStatusesFlow(): Flow<Either<Error, List<CryptoCurrencyStatus>>> {
@ -167,6 +183,7 @@ internal class CurrenciesStatusesOperations(
currencies = nonEmptyCurrencies,
maybeNetworkStatuses = null,
maybeQuotes = null,
maybeYieldBalances = null,
)
emit(maybeLoadingCurrenciesStatuses)
@ -176,8 +193,9 @@ internal class CurrenciesStatusesOperations(
val currenciesFlow = combine(
getQuotes(currenciesIds),
getNetworksStatuses(networks),
) { maybeQuotes, maybeNetworksStatuses ->
createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses)
getYieldBalances(),
) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances ->
createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses, maybeYieldBalances)
}
emitAll(currenciesFlow)
@ -255,8 +273,10 @@ internal class CurrenciesStatusesOperations(
}
}
return combine(quoteFlow, statusFlow) { maybeQuote, maybeNetworkStatus ->
createCurrencyStatus(currency, maybeQuote, maybeNetworkStatus)
val yieldBalanceFlow = getYieldBalance(currency)
return combine(quoteFlow, statusFlow, yieldBalanceFlow) { maybeQuote, maybeNetworkStatus, maybeYieldBalance ->
createCurrencyStatus(currency, maybeQuote, maybeNetworkStatus, maybeYieldBalance)
}
}
@ -264,6 +284,7 @@ internal class CurrenciesStatusesOperations(
currencies: NonEmptyList<CryptoCurrency>,
maybeQuotes: Either<Error, Set<Quote>>?,
maybeNetworkStatuses: Either<Error, Set<NetworkStatus>>?,
maybeYieldBalances: Either<Error, YieldBalanceList>?,
): Either<Error, List<CryptoCurrencyStatus>> = either {
var quotesRetrievingFailed = false
@ -281,11 +302,19 @@ internal class CurrenciesStatusesOperations(
},
)
val yieldBalances = maybeYieldBalances?.getOrNull()
currencies.map { currency ->
val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId }
val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network }
createCurrencyStatus(currency, quote, networkStatus, ignoreQuote = quotesRetrievingFailed)
val yieldBalance = (yieldBalances as? YieldBalanceList.Data)?.getBalance(currency.id.rawCurrencyId)
createCurrencyStatus(
currency = currency,
quote = quote,
networkStatus = networkStatus,
ignoreQuote = quotesRetrievingFailed,
yieldBalance = yieldBalance,
)
}
}
@ -293,6 +322,7 @@ internal class CurrenciesStatusesOperations(
currency: CryptoCurrency,
maybeQuote: Either<Error, Quote?>,
maybeNetworkStatus: Either<Error, NetworkStatus?>,
maybeYieldBalance: Either<Error, YieldBalance>?,
): Either<Error, CryptoCurrencyStatus> = either {
var quoteRetrievingFailed = false
@ -301,8 +331,15 @@ internal class CurrenciesStatusesOperations(
quoteRetrievingFailed = true
null
}
val yieldBalance = maybeYieldBalance?.getOrNull()
createCurrencyStatus(currency, quote, networkStatus, ignoreQuote = quoteRetrievingFailed)
createCurrencyStatus(
currency = currency,
quote = quote,
networkStatus = networkStatus,
ignoreQuote = quoteRetrievingFailed,
yieldBalance = yieldBalance,
)
}
private fun createCurrencyStatus(
@ -310,12 +347,14 @@ internal class CurrenciesStatusesOperations(
quote: Quote?,
networkStatus: NetworkStatus?,
ignoreQuote: Boolean,
yieldBalance: YieldBalance?,
): CryptoCurrencyStatus {
val currencyStatusOperations = CurrencyStatusOperations(
currency = currency,
quote = quote,
networkStatus = networkStatus,
ignoreQuote = ignoreQuote,
yieldBalance = yieldBalance,
)
return currencyStatusOperations.createTokenStatus()
@ -396,6 +435,65 @@ internal class CurrenciesStatusesOperations(
.onEmpty { emit(Error.EmptyNetworksStatuses.left()) }
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun getYieldBalances(): EitherFlow<Error, YieldBalanceList> {
return networksRepository.getNetworkAddressesFlow(userWalletId).flatMapLatest { addresses ->
stakingRepository.getMultiYieldBalanceFlow(
userWalletId = userWalletId,
addresses = addresses,
).map<YieldBalanceList, Either<Error, YieldBalanceList>> { it.right() }
.catch { emit(Error.DataError(it).left()) }
.onEmpty { emit(Error.EmptyYieldBalances.left()) }
}
}
private suspend fun getYieldBalancesSync(): Either<Error.EmptyYieldBalances, YieldBalanceList> {
return catch(
block = {
val networkAddresses = networksRepository.getNetworkAddresses(userWalletId)
stakingRepository.getMultiYieldBalanceSync(
userWalletId,
networkAddresses,
).right()
},
catch = {
Error.EmptyYieldBalances.left()
},
)
}
private suspend fun getYieldBalanceSync(
cryptoCurrency: CryptoCurrency,
): Either<Error.EmptyYieldBalances, YieldBalance> {
return catch(
block = {
val address = networksRepository.getNetworkAddress(userWalletId, cryptoCurrency)
stakingRepository.getSingleYieldBalanceSync(
userWalletId,
address,
).right()
},
catch = {
Error.EmptyYieldBalances.left()
},
)
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun getYieldBalance(cryptoCurrency: CryptoCurrency): EitherFlow<Error, YieldBalance> {
return networksRepository.getNetworkAddressFlow(
userWalletId,
cryptoCurrency,
).flatMapLatest { address ->
stakingRepository.getSingleYieldBalanceFlow(
userWalletId = userWalletId,
address = address,
).map<YieldBalance, Either<Error, YieldBalance>> { it.right() }
.catch { emit(Error.DataError(it).left()) }
.onEmpty { emit(Error.EmptyYieldBalances.left()) }
}
}
private fun getIds(
currencies: NonEmptyList<CryptoCurrency>,
): Pair<NonEmptySet<Network>, NonEmptySet<CryptoCurrency.ID>> {
@ -413,14 +511,16 @@ internal class CurrenciesStatusesOperations(
sealed class Error {
object EmptyCurrencies : Error()
data object EmptyCurrencies : Error()
object EmptyQuotes : Error()
data object EmptyQuotes : Error()
object EmptyNetworksStatuses : Error()
data object EmptyNetworksStatuses : Error()
object UnableToCreateCurrencyStatus : Error()
data object UnableToCreateCurrencyStatus : Error()
data class DataError(val cause: Throwable) : Error()
data object EmptyYieldBalances : Error()
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.domain.tokens.operations
import com.tangem.domain.staking.model.YieldBalance
import com.tangem.domain.tokens.model.*
import java.math.BigDecimal
@ -7,6 +8,7 @@ internal class CurrencyStatusOperations(
private val currency: CryptoCurrency,
private val quote: Quote?,
private val networkStatus: NetworkStatus?,
private val yieldBalance: YieldBalance?,
private val ignoreQuote: Boolean,
) {
@ -18,7 +20,7 @@ internal class CurrencyStatusOperations(
is NetworkStatus.MissedDerivation -> createMissedDerivationStatus()
is NetworkStatus.Unreachable -> createUnreachableStatus(status)
is NetworkStatus.NoAccount -> createNoAccountStatus(status)
is NetworkStatus.Verified -> createStatus(status)
is NetworkStatus.Verified -> createStatus(status, yieldBalance)
}
}
@ -42,7 +44,7 @@ internal class CurrencyStatusOperations(
networkAddress = status.address,
)
private fun createStatus(status: NetworkStatus.Verified): CryptoCurrencyStatus.Value {
private fun createStatus(status: NetworkStatus.Verified, yieldBalance: YieldBalance?): CryptoCurrencyStatus.Value {
val amount = when (val amount = status.amounts[currency.id]) {
null -> {
return CryptoCurrencyStatus.Loading
@ -62,6 +64,7 @@ internal class CurrencyStatusOperations(
hasCurrentNetworkTransactions = hasCurrentNetworkTransactions,
pendingTransactions = currentTransactions,
networkAddress = status.address,
yieldBalance = yieldBalance,
)
currency is CryptoCurrency.Token && currency.isCustom -> CryptoCurrencyStatus.Custom(
amount = amount,
@ -71,6 +74,7 @@ internal class CurrencyStatusOperations(
hasCurrentNetworkTransactions = hasCurrentNetworkTransactions,
pendingTransactions = currentTransactions,
networkAddress = status.address,
yieldBalance = yieldBalance,
)
quote == null -> CryptoCurrencyStatus.Loading
else -> CryptoCurrencyStatus.Loaded(
@ -81,6 +85,7 @@ internal class CurrencyStatusOperations(
hasCurrentNetworkTransactions = hasCurrentNetworkTransactions,
pendingTransactions = currentTransactions,
networkAddress = status.address,
yieldBalance = yieldBalance,
)
}
}

View file

@ -1,8 +1,10 @@
package com.tangem.domain.tokens.operations
import arrow.core.NonEmptyList
import com.tangem.domain.staking.model.YieldBalance
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.utils.extensions.orZero
import java.math.BigDecimal
internal class TokenListFiatBalanceOperations(
@ -56,10 +58,13 @@ internal class TokenListFiatBalanceOperations(
currentBalance: TotalFiatBalance,
): TotalFiatBalance {
return with(currentBalance) {
val stakingBalance = (status.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero()
val fiatStakingBalance = status.fiatRate.times(stakingBalance)
(this as? TotalFiatBalance.Loaded)?.copy(
amount = this.amount + status.fiatAmount,
amount = this.amount + status.fiatAmount + fiatStakingBalance,
) ?: TotalFiatBalance.Loaded(
amount = status.fiatAmount,
amount = status.fiatAmount + fiatStakingBalance,
isAllAmountsSummarized = true,
)
}
@ -71,12 +76,13 @@ internal class TokenListFiatBalanceOperations(
): TotalFiatBalance {
return with(currentBalance) {
val isTokenAmountCanBeSummarized = status.fiatAmount != null
val yieldBalance = (status.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero()
val fiatYieldBalance = status.fiatRate?.times(yieldBalance).orZero()
(this as? TotalFiatBalance.Loaded)?.copy(
amount = this.amount + (status.fiatAmount ?: BigDecimal.ZERO),
amount = this.amount + status.fiatAmount.orZero() + fiatYieldBalance,
isAllAmountsSummarized = isTokenAmountCanBeSummarized,
) ?: TotalFiatBalance.Loaded(
amount = status.fiatAmount ?: BigDecimal.ZERO,
amount = status.fiatAmount.orZero() + fiatYieldBalance,
isAllAmountsSummarized = isTokenAmountCanBeSummarized,
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.domain.tokens.repository
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus
@ -62,8 +63,33 @@ interface NetworksRepository {
fun isNeedToCreateAccountWithoutReserve(network: Network): Boolean
/**
* Returns list of addresses and crypto currency info of added currencies of [network] in selected wallet [userWalletId]
*/
fun getNetworkAddressesFlow(userWalletId: UserWalletId, network: Network): Flow<List<CryptoCurrencyAddress>>
/**
* Returns list of addresses and crypto currency info of added currencies of [network] in selected wallet [userWalletId]
*/
suspend fun getNetworkAddresses(userWalletId: UserWalletId, network: Network): List<CryptoCurrencyAddress>
/**
* Returns address of [cryptoCurrency] in selected wallet [userWalletId]
*/
suspend fun getNetworkAddress(userWalletId: UserWalletId, currency: CryptoCurrency): CryptoCurrencyAddress
/**
* Returns address of [cryptoCurrency] in selected wallet [userWalletId]
*/
fun getNetworkAddressFlow(userWalletId: UserWalletId, currency: CryptoCurrency): Flow<CryptoCurrencyAddress>
/**
* Returns list of addresses and crypto currency info in selected wallet [userWalletId]
*/
fun getNetworkAddressesFlow(userWalletId: UserWalletId): Flow<List<CryptoCurrencyAddress>>
/**
* Returns list of addresses and crypto currency info in selected wallet [userWalletId]
*/
suspend fun getNetworkAddresses(userWalletId: UserWalletId): List<CryptoCurrencyAddress>
}

View file

@ -4,6 +4,7 @@ import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.core.error.DataError
import com.tangem.domain.staking.model.YieldBalance
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.mock.MockNetworks
import com.tangem.domain.tokens.mock.MockQuotes
@ -13,6 +14,7 @@ import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.repository.MockCurrenciesRepository
import com.tangem.domain.tokens.repository.MockNetworksRepository
import com.tangem.domain.tokens.repository.MockQuotesRepository
import com.tangem.domain.tokens.repository.MockStakingRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import junit.framework.TestCase.assertEquals
@ -121,6 +123,7 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest {
networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
),
yieldBalance = YieldBalance.Error,
),
)
}
@ -171,5 +174,6 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest {
),
quotesRepository = MockQuotesRepository(quotes),
networksRepository = MockNetworksRepository(statuses),
stakingRepository = MockStakingRepository(),
)
}

View file

@ -16,6 +16,7 @@ import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.repository.MockCurrenciesRepository
import com.tangem.domain.tokens.repository.MockNetworksRepository
import com.tangem.domain.tokens.repository.MockQuotesRepository
import com.tangem.domain.tokens.repository.MockStakingRepository
import com.tangem.domain.wallets.models.UserWalletId
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.delay
@ -316,5 +317,6 @@ internal class GetTokenListUseCaseTest {
),
quotesRepository = MockQuotesRepository(quotes),
networksRepository = MockNetworksRepository(statuses),
stakingRepository = MockStakingRepository(),
)
}

View file

@ -49,7 +49,8 @@ internal object MockTokenLists {
val loadingUngroupedTokenList = with(failedUngroupedTokenList) {
copy(
currencies = currencies.map { it.copy(value = CryptoCurrencyStatus.Loading) }.toNonEmptyListOrNull() ?: emptyList(),
currencies = currencies.map { it.copy(value = CryptoCurrencyStatus.Loading) }.toNonEmptyListOrNull()
?: emptyList(),
totalFiatBalance = TotalFiatBalance.Loading,
)
}
@ -60,7 +61,9 @@ internal object MockTokenLists {
groups = groups.map { group ->
group.copy(
currencies = group.currencies
.map { it.copy(value = CryptoCurrencyStatus.Loading) },
.map { it.copy(value = CryptoCurrencyStatus.Loading) }
.toNonEmptyListOrNull()
?: emptyList(),
)
}.toNonEmptyListOrNull()!!,
)
@ -109,7 +112,7 @@ internal object MockTokenLists {
val sortedGroupedTokenList: TokenList.GroupedByNetwork
get() {
val groups = sortedNetworksGroups
val groups = sortedNetworksGroups.toNonEmptyList()
return unsortedGroupedTokenList.copy(
groups = groups,

View file

@ -1,6 +1,7 @@
package com.tangem.domain.tokens.mock
import arrow.core.nonEmptyListOf
import com.tangem.domain.staking.model.YieldBalance
import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.NetworkAddress
@ -151,6 +152,7 @@ internal object MockTokensStates {
pendingTransactions = emptySet(),
hasCurrentNetworkTransactions = false,
networkAddress = requireNotNull(networkStatus.value as? NetworkStatus.Verified).address,
yieldBalance = YieldBalance.Error,
),
)
}
@ -166,6 +168,7 @@ internal object MockTokensStates {
.first { it.network == status.currency.network }
.value as? NetworkStatus.Verified,
).address,
yieldBalance = YieldBalance.Error,
),
)
}

View file

@ -3,13 +3,15 @@ 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.model.CryptoCurrencyAddress
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.utils.toLce
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
@ -44,10 +46,38 @@ internal class MockNetworksRepository(
}
override fun isNeedToCreateAccountWithoutReserve(network: Network) = false
override fun getNetworkAddressesFlow(
userWalletId: UserWalletId,
network: Network,
): Flow<List<CryptoCurrencyAddress>> = channelFlow {
send(emptyList())
}
override fun getNetworkAddressesFlow(userWalletId: UserWalletId): Flow<List<CryptoCurrencyAddress>> = channelFlow {
send(emptyList())
}
override suspend fun getNetworkAddresses(
userWalletId: UserWalletId,
network: Network,
): List<CryptoCurrencyAddress> {
return emptyList()
}
override suspend fun getNetworkAddresses(userWalletId: UserWalletId): List<CryptoCurrencyAddress> {
return emptyList()
}
override suspend fun getNetworkAddress(
userWalletId: UserWalletId,
currency: CryptoCurrency,
): CryptoCurrencyAddress = CryptoCurrencyAddress(currency, "")
override fun getNetworkAddressFlow(
userWalletId: UserWalletId,
currency: CryptoCurrency,
): Flow<CryptoCurrencyAddress> = channelFlow {
send(CryptoCurrencyAddress(currency, ""))
}
}

View file

@ -0,0 +1,203 @@
package com.tangem.domain.tokens.repository
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.lce.lceFlow
import com.tangem.domain.staking.model.*
import com.tangem.domain.staking.model.action.EnterAction
import com.tangem.domain.staking.model.action.StakingActionStatus
import com.tangem.domain.staking.model.action.StakingActionType
import com.tangem.domain.staking.model.transaction.StakingTransaction
import com.tangem.domain.staking.model.transaction.StakingTransactionStatus
import com.tangem.domain.staking.model.transaction.StakingTransactionType
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import org.joda.time.DateTime
import java.math.BigDecimal
class MockStakingRepository : StakingRepository {
override fun isStakingSupported(currencyId: String): Boolean = true
override suspend fun fetchEnabledYields(refresh: Boolean) { /* no-op */
}
override suspend fun getEntryInfo(integrationId: String): StakingEntryInfo = StakingEntryInfo(
interestRate = 1.toBigDecimal(),
periodInDays = 2,
tokenSymbol = "SOL",
)
override suspend fun getYield(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Yield = Yield(
id = "1",
token = Token(
name = "Solana",
network = NetworkType.SOLANA,
symbol = "SOL",
decimals = 18,
address = null,
coinGeckoId = "solana",
logoURI = null,
isPoints = null,
),
tokens = listOf(),
args = Yield.Args(
enter = Yield.Args.Enter(
addresses = Yield.Args.Enter.Addresses(
address = AddressArgument(
required = false,
network = null,
minimum = null,
maximum = null,
),
additionalAddresses = mapOf(),
),
args = mapOf(),
),
exit = null,
),
status = Yield.Status(enter = false, exit = null),
apy = 1.toBigDecimal(),
rewardRate = 2.3,
rewardType = Yield.RewardType.APR,
metadata = Yield.Metadata(
name = "Yield",
logoUri = "",
description = "",
documentation = null,
gasFeeToken = Token(
name = "Solana",
network = NetworkType.SOLANA,
symbol = "SOL",
decimals = 18,
address = null,
coinGeckoId = null,
logoURI = null,
isPoints = null,
),
token = Token(
name = "Solana",
network = NetworkType.SOLANA,
symbol = "SOL",
decimals = 18,
address = null,
coinGeckoId = null,
logoURI = null,
isPoints = null,
),
tokens = listOf(),
type = "auto",
rewardSchedule = "1",
cooldownPeriod = Yield.Metadata.Period(days = 1),
warmupPeriod = Yield.Metadata.Period(days = 1),
rewardClaiming = "1",
defaultValidator = null,
minimumStake = null,
supportsMultipleValidators = false,
revshare = Yield.Metadata.Enabled(enabled = false),
fee = Yield.Metadata.Enabled(enabled = false),
),
validators = listOf(),
isAvailable = false,
)
override suspend fun getStakingAvailabilityForActions(
cryptoCurrencyId: CryptoCurrency.ID,
symbol: String,
): StakingAvailability = StakingAvailability.Unavailable
override suspend fun fetchSingleYieldBalance(
userWalletId: UserWalletId,
address: CryptoCurrencyAddress,
refresh: Boolean,
) {
/* no-op */
}
override fun getSingleYieldBalanceFlow(
userWalletId: UserWalletId,
address: CryptoCurrencyAddress,
): Flow<YieldBalance> = channelFlow {
send(YieldBalance.Error)
}
override suspend fun getSingleYieldBalanceSync(
userWalletId: UserWalletId,
address: CryptoCurrencyAddress,
): YieldBalance = YieldBalance.Error
override suspend fun fetchMultiYieldBalance(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
refresh: Boolean,
) {
/* no-op */
}
override fun getMultiYieldBalanceFlow(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
): Flow<YieldBalanceList> = channelFlow {
send(
YieldBalanceList.Data(
balances = listOf(YieldBalance.Error),
),
)
}
override fun getMultiYieldBalanceLce(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
): LceFlow<Throwable, YieldBalanceList> = lceFlow {
send(
YieldBalanceList.Data(
balances = listOf(YieldBalance.Error),
),
)
}
override suspend fun getMultiYieldBalanceSync(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
): YieldBalanceList = YieldBalanceList.Data(
balances = listOf(YieldBalance.Error),
)
override suspend fun createEnterAction(
integrationId: String,
amount: BigDecimal,
address: String,
validatorAddress: String,
token: Token,
): EnterAction = EnterAction(
id = "quis",
integrationId = "persequeris",
status = StakingActionStatus.PROCESSING,
type = StakingActionType.CLAIM_REWARDS,
currentStepIndex = 8701,
amount = BigDecimal.ZERO,
validatorAddress = null,
validatorAddresses = listOf(),
transactions = listOf(),
createdAt = DateTime.now(),
)
override suspend fun constructTransaction(transactionId: String): StakingTransaction = StakingTransaction(
id = "id",
network = NetworkType.SOLANA,
status = StakingTransactionStatus.SIGNED,
type = StakingTransactionType.FREEZE_ENERGY,
hash = null,
signedTransaction = null,
unsignedTransaction = null,
stepIndex = 9368,
error = null,
gasEstimate = null,
stakeId = null,
explorerUrl = null,
ledgerHwAppId = null,
isMessage = false,
)
}

View file

@ -34,6 +34,7 @@ dependencies {
implementation(projects.domain.txhistory.models)
implementation(projects.features.swap.domain.api)
implementation(projects.features.swap.domain.models)
implementation(projects.domain.staking)
/** Core modules */
implementation(projects.core.utils)

View file

@ -4,6 +4,7 @@ import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.GetCardTokensListUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -95,12 +96,14 @@ class SwapDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
dispatchers: CoroutineDispatcherProvider,
): GetCryptoCurrencyStatusesSyncUseCase {
return GetCryptoCurrencyStatusesSyncUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
dispatchers = dispatchers,
)
}
@ -112,11 +115,13 @@ class SwapDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
): GetCardTokensListUseCase {
return GetCardTokensListUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
)
}

View file

@ -35,6 +35,7 @@ dependencies {
implementation(projects.features.swap.domain)
implementation(projects.features.swap.domain.api)
implementation(projects.features.swap.domain.models)
implementation(projects.domain.staking)
/** AndroidX */
implementation(deps.androidx.activity.compose)

View file

@ -1,10 +1,10 @@
package com.tangem.feature.swap.di
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.feature.swap.domain.*
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -23,11 +23,13 @@ class SwapPresentationModule {
dispatcherProvider: CoroutineDispatcherProvider,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
): GetCryptoCurrencyStatusSyncUseCase {
return GetCryptoCurrencyStatusSyncUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
dispatchers = dispatcherProvider,
)
}

View file

@ -129,6 +129,7 @@ internal object TokenDetailsPreviewData {
onBalanceSelect = {},
displayCryptoBalance = "966,96 XLM",
displayFiatBalance = "91,50$",
isBalanceSelectorEnabled = false,
)
val balanceError = TokenDetailsBalanceBlockState.Error(
actionButtons = actionButtons,

View file

@ -25,6 +25,7 @@ internal sealed class TokenDetailsBalanceBlockState {
val onBalanceSelect: (TokenBalanceSegmentedButtonConfig) -> Unit,
val displayCryptoBalance: String,
val displayFiatBalance: String,
val isBalanceSelectorEnabled: Boolean,
) : TokenDetailsBalanceBlockState()
data class Error(

View file

@ -19,9 +19,11 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.component
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsTxHistoryTransactionStateConverter
import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import com.tangem.utils.isNullOrZero
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import java.math.BigDecimal
@ -32,21 +34,17 @@ internal class TokenDetailsLoadedBalanceConverter(
private val symbol: String,
private val decimals: Int,
private val clickIntents: TokenDetailsClickIntents,
) : Converter<TokenDetailsLoadedBalanceConverter.Data, TokenDetailsState> {
private val stakingFeatureToggles: StakingFeatureToggles,
) : Converter<Either<CurrencyStatusError, CryptoCurrencyStatus>, TokenDetailsState> {
private val txHistoryItemConverter by lazy {
TokenDetailsTxHistoryTransactionStateConverter(symbol, decimals, clickIntents)
}
data class Data(
val maybeCryptoCurrencyStatus: Either<CurrencyStatusError, CryptoCurrencyStatus>,
val maybeYieldBalance: Either<Throwable, YieldBalance>?,
)
override fun convert(value: Data): TokenDetailsState {
return value.maybeCryptoCurrencyStatus.fold(
override fun convert(value: Either<CurrencyStatusError, CryptoCurrencyStatus>): TokenDetailsState {
return value.fold(
ifLeft = { convertError() },
ifRight = { convert(it, value.maybeYieldBalance?.getOrNull() ?: YieldBalance.Empty) },
ifRight = { convert(it) },
)
}
@ -64,23 +62,17 @@ internal class TokenDetailsLoadedBalanceConverter(
)
}
private fun convert(status: CryptoCurrencyStatus, yieldBalance: YieldBalance): TokenDetailsState {
private fun convert(status: CryptoCurrencyStatus): TokenDetailsState {
val state = currentStateProvider()
val currencyName = state.marketPriceBlockState.currencySymbol
val pendingTxs = status.value.pendingTransactions.map(txHistoryItemConverter::convert).toPersistentList()
val stakingCryptoAmount = (yieldBalance as? YieldBalance.Data)?.let {
yieldBalance.balance.items.sumOf { it.amount }
}
val stakingFiatAmount = stakingCryptoAmount?.let { status.value.fiatRate?.multiply(it) }
return state.copy(
tokenBalanceBlockState = getBalanceState(
currentState = state.tokenBalanceBlockState,
status = status,
stakingCryptoAmount = stakingCryptoAmount,
stakingFiatAmount = stakingFiatAmount,
),
stakingBlocksState = getYieldBalance(status, yieldBalance, state),
stakingBlocksState = getYieldBalance(status, state),
marketPriceBlockState = getMarketPriceState(status = status.value, currencySymbol = currencyName),
pendingTxs = pendingTxs,
txHistoryState = if (state.txHistoryState is TxHistoryState.NotSupported) {
@ -94,9 +86,10 @@ internal class TokenDetailsLoadedBalanceConverter(
private fun getBalanceState(
currentState: TokenDetailsBalanceBlockState,
status: CryptoCurrencyStatus,
stakingCryptoAmount: BigDecimal?,
stakingFiatAmount: BigDecimal?,
): TokenDetailsBalanceBlockState {
val stakingCryptoAmount = (status.value.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance()
val stakingFiatAmount = stakingCryptoAmount?.let { status.value.fiatRate?.multiply(it) }
val isBalanceSelectorEnabled = stakingFeatureToggles.isStakingEnabled && !stakingCryptoAmount.isNullOrZero()
return when (status.value) {
is CryptoCurrencyStatus.NoQuote,
is CryptoCurrencyStatus.Loaded,
@ -120,6 +113,7 @@ internal class TokenDetailsLoadedBalanceConverter(
balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig,
onBalanceSelect = clickIntents::onBalanceSelect,
selectedBalanceType = currentState.selectedBalanceType,
isBalanceSelectorEnabled = isBalanceSelectorEnabled,
)
is CryptoCurrencyStatus.Loading -> TokenDetailsBalanceBlockState.Loading(
actionButtons = currentState.actionButtons,
@ -137,19 +131,14 @@ internal class TokenDetailsLoadedBalanceConverter(
}
}
private fun getYieldBalance(
status: CryptoCurrencyStatus,
yieldBalance: YieldBalance,
state: TokenDetailsState,
): StakingBlocksState {
val stakingCryptoAmount = (yieldBalance as? YieldBalance.Data)?.let {
yieldBalance.balance.items.sumOf { it.amount }
}
private fun getYieldBalance(status: CryptoCurrencyStatus, state: TokenDetailsState): StakingBlocksState {
val yieldBalance = status.value.yieldBalance as? YieldBalance.Data
val stakingCryptoAmount = yieldBalance?.getTotalStakingBalance()
val stakingRewardAmount = yieldBalance?.getRewardStakingBalance()
val stakingFiatAmount = stakingCryptoAmount?.let { status.value.fiatRate?.multiply(it) }
val stakingRewardAmount = (yieldBalance as? YieldBalance.Data)?.let {
yieldBalance.balance.items.sumOf { it.amount.multiply(it.pricePerShare) }
}
val stakingBalance = if (stakingCryptoAmount == null) {
val stakingBalance = if (stakingCryptoAmount.isNullOrZero()) {
StakingBalance.Empty
} else {
StakingBalance.Content(

View file

@ -18,7 +18,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.YieldBalance
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
@ -32,6 +31,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.t
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter.TokenDetailsLoadingTxHistoryModel
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Provider
@ -46,6 +46,7 @@ internal class TokenDetailsStateFactory(
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
private val clickIntents: TokenDetailsClickIntents,
private val featureToggles: TokenDetailsFeatureToggles,
stakingFeatureToggles: StakingFeatureToggles,
symbol: String,
decimals: Int,
) {
@ -68,6 +69,7 @@ internal class TokenDetailsStateFactory(
symbol = symbol,
decimals = decimals,
clickIntents = clickIntents,
stakingFeatureToggles = stakingFeatureToggles,
)
}
@ -121,11 +123,8 @@ internal class TokenDetailsStateFactory(
fun getCurrencyLoadedBalanceState(
cryptoCurrencyEither: Either<CurrencyStatusError, CryptoCurrencyStatus>,
yieldBalanceEither: Either<Throwable, YieldBalance>?,
): TokenDetailsState {
return tokenDetailsLoadedBalanceConverter.convert(
TokenDetailsLoadedBalanceConverter.Data(cryptoCurrencyEither, yieldBalanceEither),
)
return tokenDetailsLoadedBalanceConverter.convert(cryptoCurrencyEither)
}
fun getManageButtonsState(actions: List<TokenActionsState.ActionState>): TokenDetailsState {

View file

@ -108,7 +108,6 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
modifier = itemModifier,
isBalanceHidden = state.isBalanceHidden,
state = state.tokenBalanceBlockState,
isStakingAvailable = state.isStakingBlockShown,
)
}
items(

View file

@ -29,7 +29,6 @@ import kotlinx.collections.immutable.toImmutableList
internal fun TokenDetailsBalanceBlock(
state: TokenDetailsBalanceBlockState,
isBalanceHidden: Boolean,
isStakingAvailable: Boolean,
modifier: Modifier = Modifier,
) {
Surface(
@ -54,7 +53,7 @@ internal fun TokenDetailsBalanceBlock(
.weight(1f)
.padding(top = TangemTheme.dimens.spacing12),
)
if (isStakingAvailable) BalanceButtons(state)
BalanceButtons(state)
}
FiatBalance(
state = state,
@ -138,7 +137,7 @@ private fun CryptoBalance(
@Composable
private fun BalanceButtons(state: TokenDetailsBalanceBlockState) {
if (state !is TokenDetailsBalanceBlockState.Content) return
if (state !is TokenDetailsBalanceBlockState.Content || !state.isBalanceSelectorEnabled) return
SegmentedButtons(
config = state.balanceSegmentedButtonConfig,
@ -172,7 +171,7 @@ private fun Preview_TokenDetailsBalanceBlock(
@PreviewParameter(TokenDetailsBalanceBlockStateProvider::class) state: TokenDetailsBalanceBlockState,
) {
TangemThemePreview {
TokenDetailsBalanceBlock(state = state, isBalanceHidden = false, isStakingAvailable = true)
TokenDetailsBalanceBlock(state = state, isBalanceHidden = false)
}
}

View file

@ -30,7 +30,6 @@ import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
import com.tangem.domain.staking.GetStakingEntryInfoUseCase
import com.tangem.domain.staking.GetStakingYieldBalanceUseCase
import com.tangem.domain.staking.GetYieldUseCase
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.tokens.*
@ -108,7 +107,6 @@ internal class TokenDetailsViewModel @Inject constructor(
private val stakingFeatureToggles: StakingFeatureToggles,
private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase,
private val getYieldUseCase: GetYieldUseCase,
private val getStakingYieldBalanceUseCase: GetStakingYieldBalanceUseCase,
private val swapRepository: SwapRepository,
private val swapTransactionRepository: SwapTransactionRepository,
private val quotesRepository: QuotesRepository,
@ -135,7 +133,6 @@ internal class TokenDetailsViewModel @Inject constructor(
?: error("This screen can't open without `CryptoCurrency`")
private val userWallet: UserWallet
private var stakingIntegrationId: String? = null
lateinit var router: InnerTokenDetailsRouter
@ -157,6 +154,7 @@ internal class TokenDetailsViewModel @Inject constructor(
symbol = cryptoCurrency.symbol,
decimals = cryptoCurrency.decimals,
featureToggles = tokenDetailsFeatureToggles,
stakingFeatureToggles = stakingFeatureToggles,
)
private val exchangeStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
@ -220,6 +218,7 @@ internal class TokenDetailsViewModel @Inject constructor(
}
private fun updateContent() {
subscribeOnCurrencyStatusUpdates()
subscribeOnExchangeTransactionsUpdates()
updateTxHistory(refresh = false, showItemsLoading = true)
@ -281,36 +280,12 @@ internal class TokenDetailsViewModel @Inject constructor(
)
.distinctUntilChanged()
.onEach { maybeCurrencyStatus ->
maybeCurrencyStatus.fold(
ifLeft = {
internalUiState.value = stateFactory.getCurrencyLoadedBalanceState(
maybeCurrencyStatus,
null,
)
},
ifRight = { status ->
cryptoCurrencyStatus = status
if (stakingIntegrationId != null) {
getStakingYieldBalanceUseCase(
userWalletId = userWalletId,
address = status.value.networkAddress?.defaultAddress?.value.orEmpty(),
integrationId = stakingIntegrationId.orEmpty(),
).onEach { maybeYieldBalance ->
internalUiState.value = stateFactory.getCurrencyLoadedBalanceState(
maybeCurrencyStatus,
maybeYieldBalance,
)
}.launchIn(viewModelScope)
} else {
internalUiState.value =
stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus, null)
}
updateButtons(currencyStatus = status)
updateWarnings(status)
},
)
internalUiState.value = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus)
maybeCurrencyStatus.onRight { status ->
cryptoCurrencyStatus = status
updateButtons(currencyStatus = status)
updateWarnings(status)
}
currencyStatusAnalyticsSender.send(maybeCurrencyStatus)
}
.flowOn(dispatchers.main)
@ -411,12 +386,8 @@ internal class TokenDetailsViewModel @Inject constructor(
)
internalUiState.value = stateFactory.getStateWithUpdatedStakingAvailability(stakingAvailability)
if (stakingAvailability is StakingAvailability.Available) {
stakingIntegrationId = stakingAvailability.integrationId
val stakingInfo = getStakingEntryInfoUseCase(stakingAvailability.integrationId)
subscribeOnCurrencyStatusUpdates()
internalUiState.value = stateFactory.getStateWithStaking(stakingInfo)
} else {
subscribeOnCurrencyStatusUpdates()
}
}
}

View file

@ -5,11 +5,13 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.YieldBalance
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.orZero
import java.math.BigDecimal
internal class TokenItemStateConverter(
@ -61,13 +63,16 @@ internal class TokenItemStateConverter(
}
private fun CryptoCurrencyStatus.getFormattedAmount(): String {
val amount = value.amount ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN
val yieldBalance = (value.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero()
val amount = value.amount?.plus(yieldBalance) ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN
return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals)
}
private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String {
val fiatAmount = value.fiatAmount ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN
val yieldBalance = (value.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero()
val fiatYieldBalance = value.fiatRate?.times(yieldBalance).orZero()
val fiatAmount = value.fiatAmount?.plus(fiatYieldBalance) ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN
val appCurrency = appCurrencyProvider()
return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol)