From 1d75e884c86ffd0fd76a49a3eb14bbf48932dbd9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 21 Jun 2024 21:35:19 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/di/domain/StakingDomainModule.kt | 18 ++- .../models/request/YieldBalanceRequestBody.kt | 1 + .../local/token/DefaultStakingBalanceStore.kt | 33 ++++- .../local/token/StakingBalanceStore.kt | 13 +- data/staking/build.gradle.kts | 7 + .../data/staking/DefaultStakingRepository.kt | 136 ++++++++++++++++-- .../converters/YieldBalanceConverter.kt | 16 +++ .../converters/YieldBalanceListConverter.kt | 18 +++ .../data/staking/di/StakingDataModule.kt | 6 + domain/staking/build.gradle.kts | 1 + .../domain/staking/model/YieldBalance.kt | 21 +++ .../domain/staking/model/YieldBalanceList.kt | 5 + .../FetchStakingYieldBalanceUseCase.kt | 25 ++++ .../staking/GetStakingYieldBalanceUseCase.kt | 30 ++++ .../staking/repositories/StakingRepository.kt | 33 ++++- 15 files changed, 340 insertions(+), 23 deletions(-) create mode 100644 data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt create mode 100644 domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt create mode 100644 domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalanceList.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index 790bb01a7a..2a6fb034b9 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -1,10 +1,6 @@ package com.tangem.tap.di.domain -import com.tangem.domain.settings.* -import com.tangem.domain.staking.GetStakingAvailabilityUseCase -import com.tangem.domain.staking.GetStakingEntryInfoUseCase -import com.tangem.domain.staking.FetchStakingTokensUseCase -import com.tangem.domain.staking.GetYieldUseCase +import com.tangem.domain.staking.* import com.tangem.domain.staking.repositories.StakingRepository import dagger.Module import dagger.Provides @@ -47,4 +43,16 @@ internal object StakingDomainModule { stakingRepository = stakingRepository, ) } + + @Provides + @Singleton + fun provideFetchStakingYieldBalanceUseCase(stakingRepository: StakingRepository): FetchStakingYieldBalanceUseCase { + return FetchStakingYieldBalanceUseCase(stakingRepository = stakingRepository) + } + + @Provides + @Singleton + fun provideGetStakingYieldBalanceUseCase(stakingRepository: StakingRepository): GetStakingYieldBalanceUseCase { + return GetStakingYieldBalanceUseCase(stakingRepository = stakingRepository) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt index 3d4947c14a..bbfe66a6af 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt @@ -5,6 +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, ) { data class YieldBalanceRequestArgs( diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt index c905706748..c88970b238 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt @@ -1,25 +1,50 @@ package com.tangem.datasource.local.token import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map internal class DefaultStakingBalanceStore( - private val dataStore: StringKeyDataStore>, + private val dataStore: StringKeyDataStore>, ) : StakingBalanceStore { - override fun get(): Flow> { + override fun get(): Flow> { return dataStore.get(STAKING_BALANCE_KEY) } - override suspend fun getSyncOrNull(): List? { + override suspend fun getSyncOrNull(): List? { return dataStore.getSyncOrNull(STAKING_BALANCE_KEY) } - override suspend fun store(items: List) { + override suspend fun store(items: List) { return dataStore.store(STAKING_BALANCE_KEY, items) } + override fun get(integrationId: String): Flow> { + return dataStore.get(STAKING_BALANCE_KEY) + .map { balances -> + balances.filter { it.integrationId == integrationId } + .flatMap { it.balances } + } + } + + override suspend fun getSyncOrNull(integrationId: String): List? { + return dataStore.getSyncOrNull(STAKING_BALANCE_KEY) + ?.firstOrNull { it.integrationId == integrationId }?.balances + } + + override suspend fun store(integrationId: String, item: YieldBalanceWrapperDTO) { + val balances = dataStore.getSyncOrNull(STAKING_BALANCE_KEY) + ?.toMutableList() + ?.addOrReplace(item) { item.integrationId == integrationId } + ?: listOf(item) + + return dataStore.store(STAKING_BALANCE_KEY, balances) + } + companion object { private const val STAKING_BALANCE_KEY = "STAKING_BALANCE_KEY" } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt index b7260b675b..0a9ea06c9e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt @@ -1,13 +1,20 @@ package com.tangem.datasource.local.token import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import kotlinx.coroutines.flow.Flow interface StakingBalanceStore { - fun get(): Flow> + fun get(): Flow> - suspend fun getSyncOrNull(): List? + suspend fun getSyncOrNull(): List? - suspend fun store(items: List) + suspend fun store(items: List) + + fun get(integrationId: String): Flow> + + suspend fun getSyncOrNull(integrationId: String): List? + + suspend fun store(integrationId: String, item: YieldBalanceWrapperDTO) } \ No newline at end of file diff --git a/data/staking/build.gradle.kts b/data/staking/build.gradle.kts index 186d53da6e..e0b3458636 100644 --- a/data/staking/build.gradle.kts +++ b/data/staking/build.gradle.kts @@ -12,10 +12,17 @@ android { dependencies { + /** Core modules */ implementation(projects.core.datasource) implementation(projects.core.utils) + + /** Common modules */ + implementation(projects.data.common) + + /** Domain modules */ implementation(projects.domain.tokens.models) implementation(projects.domain.staking) + implementation(projects.domain.wallets.models) // region DI diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index f0e5a33941..379877723a 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -2,23 +2,33 @@ package com.tangem.data.staking import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toCoinId -import com.tangem.data.staking.converters.StakingNetworkTypeConverter -import com.tangem.data.staking.converters.TokenConverter -import com.tangem.data.staking.converters.YieldConverter +import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.staking.converters.* import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.stakekit.StakeKitApi +import com.tangem.datasource.api.stakekit.models.request.Address +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.datasource.local.token.StakingYieldsStore -import com.tangem.domain.staking.model.StakingAvailability -import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.Yield +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.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.cancellable +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext internal class DefaultStakingRepository( private val stakeKitApi: StakeKitApi, private val stakingYieldsStore: StakingYieldsStore, + private val stakingBalanceStore: StakingBalanceStore, + private val cacheRegistry: CacheRegistry, private val dispatchers: CoroutineDispatcherProvider, ) : StakingRepository { @@ -31,6 +41,10 @@ internal class DefaultStakingRepository( tokenConverter = tokenConverter, ) + private val yieldBalanceConverter = YieldBalanceConverter() + + private val yieldBalanceListConverter = YieldBalanceListConverter() + override fun isStakingSupported(currencyId: String): Boolean { return integrationIds.contains(currencyId) } @@ -94,9 +108,93 @@ internal class DefaultStakingRepository( } } + override suspend fun fetchSingleYieldBalance( + userWalletId: UserWalletId, + networkStatus: NetworkStatus, + integrationId: String, + refresh: Boolean, + ) = withContext(dispatchers.io) { + cacheRegistry.invokeOnExpire( + key = getYieldBalancesKey(userWalletId), + skipCache = refresh, + block = { + val result = stakeKitApi.getSingleYieldBalance( + integrationId = integrationId, + body = getBalanceRequestData(networkStatus), + ).getOrThrow() + + stakingBalanceStore.store( + integrationId, + YieldBalanceWrapperDTO( + balances = result, + integrationId = integrationId, + ), + ) + }, + ) + } + + override fun getSingleYieldBalanceFlow( + userWalletId: UserWalletId, + networkStatus: NetworkStatus, + integrationId: String, + ): Flow> = channelFlow { + launch(dispatchers.io) { + stakingBalanceStore.get(integrationId) + .collectLatest { + send(yieldBalanceConverter.convertList(it)) + } + } + + withContext(dispatchers.io) { + fetchSingleYieldBalance( + userWalletId, + networkStatus, + integrationId, + ) + } + }.cancellable() + + override suspend fun fetchMultiYieldBalance( + userWalletId: UserWalletId, + networks: Set, + integrationId: String, + refresh: Boolean, + ) = withContext(dispatchers.io) { + cacheRegistry.invokeOnExpire( + key = getYieldBalancesKey(userWalletId), + skipCache = refresh, + block = { + val result = stakeKitApi.getMultipleYieldBalances( + networks.map(::getBalanceRequestData), + ).getOrThrow() + + stakingBalanceStore.store(result) + }, + ) + } + + override fun getMultiYieldBalanceFlow( + userWalletId: UserWalletId, + networks: Set, + integrationId: String, + ): Flow> = channelFlow { + launch(dispatchers.io) { + stakingBalanceStore.get() + .collectLatest { send(yieldBalanceListConverter.convertList(it)) } + } + + withContext(dispatchers.io) { + fetchMultiYieldBalance( + userWalletId, + networks, + integrationId, + ) + } + }.cancellable() + private fun findPrefetchedYield(yields: List, currencyId: String, symbol: String): Yield? { - return yields - .find { it.token.coinGeckoId == currencyId && it.token.symbol == symbol } + return yields.find { it.token.coinGeckoId == currencyId && it.token.symbol == symbol } } private suspend fun getEnabledYields(): List? { @@ -104,6 +202,28 @@ internal class DefaultStakingRepository( return yields.map { yieldConverter.convert(it) } } + private fun getBalanceRequestData(networkStatus: NetworkStatus): YieldBalanceRequestBody { + val networkAddress = when (val network = networkStatus.value) { + NetworkStatus.MissedDerivation -> null + is NetworkStatus.NoAccount -> network.address + is NetworkStatus.Unreachable -> network.address + is NetworkStatus.Verified -> network.address + } + val address = networkAddress?.defaultAddress?.value.orEmpty() + return YieldBalanceRequestBody( + addresses = Address( + address = address, + additionalAddresses = null, // todo fill additional addresses metadata if needed + explorerUrl = "", // todo fill exporer url + ), + args = YieldBalanceRequestBody.YieldBalanceRequestArgs( + validatorAddresses = listOf(), // todo add validator addresses + ), + ) + } + + private fun getYieldBalancesKey(userWalletId: UserWalletId) = "yield_balance_${userWalletId.stringValue}" + companion object { private val integrationIds = setOf( Blockchain.Solana.toCoinId(), diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt new file mode 100644 index 0000000000..7a3182cc87 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt @@ -0,0 +1,16 @@ +package com.tangem.data.staking.converters + +import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO +import com.tangem.domain.staking.model.BalanceType +import com.tangem.domain.staking.model.YieldBalance +import com.tangem.utils.converter.Converter + +internal class YieldBalanceConverter : Converter { + override fun convert(value: BalanceDTO): YieldBalance { + return YieldBalance( + type = BalanceType.valueOf(value.type.name), + amount = value.amount, + pricePerShare = value.pricePerShare, + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt new file mode 100644 index 0000000000..d0c4eea878 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt @@ -0,0 +1,18 @@ +package com.tangem.data.staking.converters + +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.domain.staking.model.YieldBalanceList +import com.tangem.utils.converter.Converter + +internal class YieldBalanceListConverter : Converter { + + internal val converter by lazy(LazyThreadSafetyMode.NONE) { + YieldBalanceConverter() + } + + override fun convert(value: YieldBalanceWrapperDTO): YieldBalanceList { + return YieldBalanceList( + balances = converter.convertList(value.balances), + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index 0b47b925d1..16c1bed364 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -1,7 +1,9 @@ package com.tangem.data.staking.di +import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.staking.DefaultStakingRepository 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.utils.coroutines.CoroutineDispatcherProvider @@ -20,12 +22,16 @@ internal object StakingDataModule { fun provideStakingRepository( stakeKitApi: StakeKitApi, stakingTokenStore: StakingYieldsStore, + stakingBalanceStore: StakingBalanceStore, dispatchers: CoroutineDispatcherProvider, + cacheRegistry: CacheRegistry, ): StakingRepository { return DefaultStakingRepository( stakeKitApi = stakeKitApi, stakingYieldsStore = stakingTokenStore, + stakingBalanceStore = stakingBalanceStore, dispatchers = dispatchers, + cacheRegistry = cacheRegistry, ) } } \ No newline at end of file diff --git a/domain/staking/build.gradle.kts b/domain/staking/build.gradle.kts index fc6ced152c..38ec70430c 100644 --- a/domain/staking/build.gradle.kts +++ b/domain/staking/build.gradle.kts @@ -17,4 +17,5 @@ dependencies { implementation(deps.kotlin.serialization) implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) } \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt new file mode 100644 index 0000000000..817743ac7c --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt @@ -0,0 +1,21 @@ +package com.tangem.domain.staking.model + +import java.math.BigDecimal + +data class YieldBalance( + val type: BalanceType, + val amount: BigDecimal, + val pricePerShare: BigDecimal, +) + +enum class BalanceType { + AVAILABLE, + STAKED, + UNSTAKING, + UNSTAKED, + PREPARING, + REWARDS, + LOCKED, + UNLOCKING, + UNKNOWN, +} \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalanceList.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalanceList.kt new file mode 100644 index 0000000000..931e8fa879 --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalanceList.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.staking.model + +data class YieldBalanceList( + val balances: List, +) \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt new file mode 100644 index 0000000000..46f0fdf101 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId + +class FetchStakingYieldBalanceUseCase( + private val stakingRepository: StakingRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + networkStatus: NetworkStatus, + integrationId: String, + refresh: Boolean = false, + ): Either = Either.catch { + stakingRepository.fetchSingleYieldBalance( + userWalletId = userWalletId, + networkStatus = networkStatus, + integrationId = integrationId, + refresh = refresh, + ) + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt new file mode 100644 index 0000000000..8821767a15 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt @@ -0,0 +1,30 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import arrow.core.left +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.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map + +class GetStakingYieldBalanceUseCase( + private val stakingRepository: StakingRepository, +) { + + operator fun invoke( + userWalletId: UserWalletId, + networkStatus: NetworkStatus, + integrationId: String, + ): EitherFlow> { + return stakingRepository.getSingleYieldBalanceFlow( + userWalletId = userWalletId, + networkStatus = networkStatus, + integrationId = integrationId, + ).map, Either>> { it.right() } + .catch { emit(it.left()) } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index 2e1fe82f5d..5eb34854c5 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -1,9 +1,10 @@ package com.tangem.domain.staking.repositories -import com.tangem.domain.staking.model.StakingAvailability -import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.* import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow interface StakingRepository { @@ -19,4 +20,30 @@ interface StakingRepository { cryptoCurrencyId: CryptoCurrency.ID, symbol: String, ): StakingAvailability + + suspend fun fetchSingleYieldBalance( + userWalletId: UserWalletId, + networkStatus: NetworkStatus, + integrationId: String, + refresh: Boolean = false, + ) + + fun getSingleYieldBalanceFlow( + userWalletId: UserWalletId, + networkStatus: NetworkStatus, + integrationId: String, + ): Flow> + + suspend fun fetchMultiYieldBalance( + userWalletId: UserWalletId, + networks: Set, + integrationId: String, + refresh: Boolean = false, + ) + + fun getMultiYieldBalanceFlow( + userWalletId: UserWalletId, + networks: Set, + integrationId: String, + ): Flow> } \ No newline at end of file