Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-21 21:35:19 +05:00
parent 0b671b4ac1
commit 1d75e884c8
15 changed files with 340 additions and 23 deletions

View file

@ -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)
}
}

View file

@ -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(

View file

@ -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<List<BalanceDTO>>,
private val dataStore: StringKeyDataStore<List<YieldBalanceWrapperDTO>>,
) : StakingBalanceStore {
override fun get(): Flow<List<BalanceDTO>> {
override fun get(): Flow<List<YieldBalanceWrapperDTO>> {
return dataStore.get(STAKING_BALANCE_KEY)
}
override suspend fun getSyncOrNull(): List<BalanceDTO>? {
override suspend fun getSyncOrNull(): List<YieldBalanceWrapperDTO>? {
return dataStore.getSyncOrNull(STAKING_BALANCE_KEY)
}
override suspend fun store(items: List<BalanceDTO>) {
override suspend fun store(items: List<YieldBalanceWrapperDTO>) {
return dataStore.store(STAKING_BALANCE_KEY, items)
}
override fun get(integrationId: String): Flow<List<BalanceDTO>> {
return dataStore.get(STAKING_BALANCE_KEY)
.map { balances ->
balances.filter { it.integrationId == integrationId }
.flatMap { it.balances }
}
}
override suspend fun getSyncOrNull(integrationId: String): List<BalanceDTO>? {
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"
}

View file

@ -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<List<BalanceDTO>>
fun get(): Flow<List<YieldBalanceWrapperDTO>>
suspend fun getSyncOrNull(): List<BalanceDTO>?
suspend fun getSyncOrNull(): List<YieldBalanceWrapperDTO>?
suspend fun store(items: List<BalanceDTO>)
suspend fun store(items: List<YieldBalanceWrapperDTO>)
fun get(integrationId: String): Flow<List<BalanceDTO>>
suspend fun getSyncOrNull(integrationId: String): List<BalanceDTO>?
suspend fun store(integrationId: String, item: YieldBalanceWrapperDTO)
}

View file

@ -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

View file

@ -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<List<YieldBalance>> = 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<NetworkStatus>,
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<NetworkStatus>,
integrationId: String,
): Flow<List<YieldBalanceList>> = channelFlow {
launch(dispatchers.io) {
stakingBalanceStore.get()
.collectLatest { send(yieldBalanceListConverter.convertList(it)) }
}
withContext(dispatchers.io) {
fetchMultiYieldBalance(
userWalletId,
networks,
integrationId,
)
}
}.cancellable()
private fun findPrefetchedYield(yields: List<Yield>, 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<Yield>? {
@ -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(),

View file

@ -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<BalanceDTO, YieldBalance> {
override fun convert(value: BalanceDTO): YieldBalance {
return YieldBalance(
type = BalanceType.valueOf(value.type.name),
amount = value.amount,
pricePerShare = value.pricePerShare,
)
}
}

View file

@ -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<YieldBalanceWrapperDTO, YieldBalanceList> {
internal val converter by lazy(LazyThreadSafetyMode.NONE) {
YieldBalanceConverter()
}
override fun convert(value: YieldBalanceWrapperDTO): YieldBalanceList {
return YieldBalanceList(
balances = converter.convertList(value.balances),
)
}
}

View file

@ -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,
)
}
}

View file

@ -17,4 +17,5 @@ dependencies {
implementation(deps.kotlin.serialization)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
}

View file

@ -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,
}

View file

@ -0,0 +1,5 @@
package com.tangem.domain.staking.model
data class YieldBalanceList(
val balances: List<YieldBalance>,
)

View file

@ -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<Throwable, Unit> = Either.catch {
stakingRepository.fetchSingleYieldBalance(
userWalletId = userWalletId,
networkStatus = networkStatus,
integrationId = integrationId,
refresh = refresh,
)
}
}

View file

@ -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<Throwable, List<YieldBalance>> {
return stakingRepository.getSingleYieldBalanceFlow(
userWalletId = userWalletId,
networkStatus = networkStatus,
integrationId = integrationId,
).map<List<YieldBalance>, Either<Throwable, List<YieldBalance>>> { it.right() }
.catch { emit(it.left()) }
}
}

View file

@ -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<List<YieldBalance>>
suspend fun fetchMultiYieldBalance(
userWalletId: UserWalletId,
networks: Set<NetworkStatus>,
integrationId: String,
refresh: Boolean = false,
)
fun getMultiYieldBalanceFlow(
userWalletId: UserWalletId,
networks: Set<NetworkStatus>,
integrationId: String,
): Flow<List<YieldBalanceList>>
}