Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-18 10:31:03 +02:00
parent 9e0b8b3234
commit b449775ad8
112 changed files with 979 additions and 740 deletions

View file

@ -25,10 +25,10 @@ import kotlinx.coroutines.withContext
import timber.log.Timber
/**
* P2P staking repository implementation
* P2PEthPool staking repository implementation
*/
internal class DefaultP2PEthPoolRepository(
private val p2pApi: P2PEthPoolApi,
private val p2pEthPoolApi: P2PEthPoolApi,
private val p2pEthPoolVaultsStore: P2PEthPoolVaultsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : P2PEthPoolRepository {
@ -41,7 +41,7 @@ internal class DefaultP2PEthPoolRepository(
override suspend fun fetchVaults(network: P2PEthPoolNetwork) {
val vaults = getVaults(network).getOrElse { error ->
Timber.e("Error fetching P2P vaults: $error")
Timber.e("Error fetching P2PEthPool vaults: $error")
emptyList()
}
p2pEthPoolVaultsStore.store(vaults)
@ -49,7 +49,7 @@ internal class DefaultP2PEthPoolRepository(
override suspend fun getVaults(network: P2PEthPoolNetwork): Either<StakingError, List<P2PEthPoolVault>> = either {
withContext(dispatchers.io) {
val response = p2pApi.getVaults(network.value)
val response = p2pEthPoolApi.getVaults(network.value)
when (response) {
is ApiResponse.Success -> {
val data = response.data
@ -76,7 +76,7 @@ internal class DefaultP2PEthPoolRepository(
vaultAddress = vaultAddress,
amount = amount.toDoubleOrNull() ?: raise(StakingError.InvalidAmount("Invalid amount format: $amount")),
)
val response = p2pApi.createDepositTransaction(network.value, requestBody)
val response = p2pEthPoolApi.createDepositTransaction(network.value, requestBody)
when (response) {
is ApiResponse.Success -> {
val data = response.data
@ -101,7 +101,7 @@ internal class DefaultP2PEthPoolRepository(
stakerPublicKey = stakerPublicKey,
stakeTransactionHash = stakeTransactionHash,
)
val response = p2pApi.createUnstakeTransaction(network.value, requestBody)
val response = p2pEthPoolApi.createUnstakeTransaction(network.value, requestBody)
when (response) {
is ApiResponse.Success -> {
val data = response.data
@ -133,7 +133,7 @@ internal class DefaultP2PEthPoolRepository(
): Either<StakingError, P2PEthPoolUnsignedTx> = either {
withContext(dispatchers.io) {
val requestBody = P2PEthPoolWithdrawRequest(stakerAddress = stakerAddress)
val response = p2pApi.createWithdrawTransaction(network.value, requestBody)
val response = p2pEthPoolApi.createWithdrawTransaction(network.value, requestBody)
when (response) {
is ApiResponse.Success -> {
val data = response.data
@ -154,7 +154,7 @@ internal class DefaultP2PEthPoolRepository(
): Either<StakingError, P2PEthPoolBroadcastResult> = either {
withContext(dispatchers.io) {
val requestBody = P2PEthPoolBroadcastRequest(signedTransaction = signedTransaction)
val response = p2pApi.broadcastTransaction(network.value, requestBody)
val response = p2pEthPoolApi.broadcastTransaction(network.value, requestBody)
when (response) {
is ApiResponse.Success -> {
val data = response.data
@ -175,7 +175,7 @@ internal class DefaultP2PEthPoolRepository(
vaultAddress: String,
): Either<StakingError, P2PEthPoolAccount> = either {
withContext(dispatchers.io) {
val response = p2pApi.getAccountInfo(network.value, delegatorAddress, vaultAddress)
val response = p2pEthPoolApi.getAccountInfo(network.value, delegatorAddress, vaultAddress)
when (response) {
is ApiResponse.Success -> {
val data = response.data
@ -197,7 +197,7 @@ internal class DefaultP2PEthPoolRepository(
period: Int?,
): Either<StakingError, List<P2PEthPoolReward>> = either {
withContext(dispatchers.io) {
val response = p2pApi.getRewards(
val response = p2pEthPoolApi.getRewards(
network = network.value,
delegatorAddress = delegatorAddress,
vaultAddress = vaultAddress,
@ -217,6 +217,10 @@ internal class DefaultP2PEthPoolRepository(
}
}
override fun getVaultsFlow(): Flow<List<P2PEthPoolVault>> {
return p2pEthPoolVaultsStore.get()
}
override fun getStakingAvailability(): Flow<StakingAvailability> {
return getVaultsFlow()
.distinctUntilChanged()
@ -224,7 +228,7 @@ internal class DefaultP2PEthPoolRepository(
if (vaults.isEmpty()) {
return@map StakingAvailability.TemporaryUnavailable
} else {
StakingAvailability.Available(StakingOption.P2P(vaults))
StakingAvailability.Available(StakingOption.P2PEthPool(vaults))
}
}
}
@ -234,15 +238,11 @@ internal class DefaultP2PEthPoolRepository(
return if (vaults.isEmpty()) {
StakingAvailability.TemporaryUnavailable
} else {
StakingAvailability.Available(StakingOption.P2P(vaults))
StakingAvailability.Available(StakingOption.P2PEthPool(vaults))
}
}
private suspend fun getVaultsSync(): List<P2PEthPoolVault> {
return p2pEthPoolVaultsStore.getSync()
}
private fun getVaultsFlow(): Flow<List<P2PEthPoolVault>> {
return p2pEthPoolVaultsStore.get()
}
}

View file

@ -364,6 +364,7 @@ internal class DefaultStakeKitRepository(
}
override fun getStakingAvailability(
integrationId: StakingIntegrationID.StakeKit,
rawCurrencyId: CryptoCurrency.RawID,
symbol: String,
): Flow<StakingAvailability> {
@ -381,7 +382,7 @@ internal class DefaultStakeKitRepository(
)
if (prefetchedYield != null) {
StakingAvailability.Available(StakingOption.StakeKit(prefetchedYield))
StakingAvailability.Available(StakingOption.StakeKit(integrationId, prefetchedYield))
} else {
StakingAvailability.TemporaryUnavailable
}
@ -389,6 +390,7 @@ internal class DefaultStakeKitRepository(
}
override suspend fun getStakingAvailabilitySync(
integrationId: StakingIntegrationID.StakeKit,
rawCurrencyId: CryptoCurrency.RawID,
symbol: String,
): StakingAvailability {
@ -404,7 +406,7 @@ internal class DefaultStakeKitRepository(
)
return if (prefetchedYield != null) {
StakingAvailability.Available(StakingOption.StakeKit(prefetchedYield))
StakingAvailability.Available(StakingOption.StakeKit(integrationId, prefetchedYield))
} else {
StakingAvailability.TemporaryUnavailable
}

View file

@ -29,12 +29,12 @@ internal class DefaultStakingErrorResolver(
is StakingError.DomainError -> {
analyticsEventHandler.send(StakingAnalyticsEvent.DomainError(error))
}
// P2P errors
// P2PEthPool errors
is StakingError.InvalidAmount,
is StakingError.DataError,
is StakingError.UnknownError,
-> {
// P2P errors - no specific analytics event yet
// P2PEthPool errors - no specific analytics event yet
}
}

View file

@ -61,8 +61,9 @@ internal class DefaultStakingRepository(
val stakingIntegration = StakingIntegrationID.create(currencyId = cryptoCurrency.id)
val availabilityFlow = when (stakingIntegration) {
is StakingIntegrationID.P2P -> p2pEthPoolRepository.getStakingAvailability()
StakingIntegrationID.P2PEthPool -> p2pEthPoolRepository.getStakingAvailability()
is StakingIntegrationID.StakeKit -> stakeKitRepository.getStakingAvailability(
stakingIntegration,
rawCurrencyId,
cryptoCurrency.symbol,
)
@ -94,8 +95,9 @@ internal class DefaultStakingRepository(
?: return StakingAvailability.Unavailable
return when (stakingIntegration) {
is StakingIntegrationID.P2P -> p2pEthPoolRepository.getStakingAvailabilitySync()
StakingIntegrationID.P2PEthPool -> p2pEthPoolRepository.getStakingAvailabilitySync()
is StakingIntegrationID.StakeKit -> stakeKitRepository.getStakingAvailabilitySync(
stakingIntegration,
rawCurrencyId,
cryptoCurrency.symbol,
)

View file

@ -5,6 +5,8 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.MetadataDTO.RewardScheduleDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.ValidatorDTO.ValidatorStatusDTO
import com.tangem.datasource.local.token.converter.YieldTokenConverter
import com.tangem.domain.staking.model.common.RewardInfo
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.domain.staking.model.stakekit.AddressArgument
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.Yield.Metadata.RewardSchedule
@ -124,7 +126,7 @@ internal object YieldConverter : Converter<YieldDTO, Yield> {
)
}
private fun convertValidator(validatorDTO: YieldDTO.ValidatorDTO, rewardType: Yield.RewardType): Yield.Validator {
private fun convertValidator(validatorDTO: YieldDTO.ValidatorDTO, rewardType: RewardType): Yield.Validator {
val address = validatorDTO.address.asMandatory("address")
return Yield.Validator(
@ -142,7 +144,7 @@ internal object YieldConverter : Converter<YieldDTO, Yield> {
)
}
private fun createRewardInfo(validatorDTO: YieldDTO.ValidatorDTO, rewardType: Yield.RewardType): Yield.RewardInfo? {
private fun createRewardInfo(validatorDTO: YieldDTO.ValidatorDTO, rewardType: RewardType): RewardInfo? {
val aprOrApy = validatorDTO.apr
val commission = validatorDTO.commission
// gross = net / (1 - commission)
@ -162,17 +164,17 @@ internal object YieldConverter : Converter<YieldDTO, Yield> {
} else {
netApy
}
grossAprOrApy?.let { Yield.RewardInfo(rate = it, type = rewardType) }
grossAprOrApy?.let { RewardInfo(rate = it, type = rewardType) }
} catch (_: Exception) {
aprOrApy?.let { Yield.RewardInfo(rate = it, type = rewardType) }
aprOrApy?.let { RewardInfo(rate = it, type = rewardType) }
}
}
private fun convertRewardType(rewardTypeDTO: YieldDTO.RewardTypeDTO): Yield.RewardType {
private fun convertRewardType(rewardTypeDTO: YieldDTO.RewardTypeDTO): RewardType {
return when (rewardTypeDTO) {
YieldDTO.RewardTypeDTO.APY -> Yield.RewardType.APY
YieldDTO.RewardTypeDTO.APR -> Yield.RewardType.APR
else -> Yield.RewardType.UNKNOWN
YieldDTO.RewardTypeDTO.APY -> RewardType.APY
YieldDTO.RewardTypeDTO.APR -> RewardType.APR
else -> RewardType.UNKNOWN
}
}

View file

@ -9,7 +9,7 @@ import com.tangem.utils.converter.Converter
import org.joda.time.Instant
/**
* Converter from P2P Account Info Response to Domain model
* Converter from P2PEthPool Account Info Response to Domain model
*/
internal object P2PEthPoolAccountConverter : Converter<P2PEthPoolAccountResponse, P2PEthPoolAccount> {

View file

@ -8,7 +8,7 @@ import com.tangem.utils.converter.Converter
import java.math.BigDecimal
/**
* Converter from P2P Broadcast Transaction Response to Domain model
* Converter from P2PEthPool Broadcast Transaction Response to Domain model
*/
internal object P2PEthPoolBroadcastResultConverter : Converter<P2PEthPoolBroadcastResponse, P2PEthPoolBroadcastResult> {

View file

@ -6,7 +6,7 @@ import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.utils.converter.Converter
/**
* Converter from P2P Error Response to Domain StakingError
* Converter from P2PEthPool Error Response to Domain StakingError
*/
@Suppress("MagicNumber")
internal object P2PEthPoolErrorConverter : Converter<P2PEthPoolErrorResponse, StakingError> {

View file

@ -5,7 +5,7 @@ import com.tangem.domain.staking.model.ethpool.P2PEthPoolReward
import com.tangem.utils.converter.Converter
/**
* Converter from P2P Reward Entry DTO to Domain model
* Converter from P2PEthPool Reward Entry DTO to Domain model
*/
internal object P2PEthPoolRewardConverter : Converter<P2PEthPoolRewardDTO, P2PEthPoolReward> {

View file

@ -9,16 +9,16 @@ import com.tangem.domain.models.staking.*
import com.tangem.domain.staking.model.StakingIntegrationID
import kotlinx.datetime.Instant
/** Converts P2P ETH Pool API response to [StakingBalance.Data.P2P] */
internal object P2PStakingBalanceConverter {
/** Converts P2PEthPool API response to [StakingBalance.Data.P2PEthPool] */
internal object P2PEthPoolStakingBalanceConverter {
fun convert(response: P2PEthPoolAccountResponse, source: StatusSource): StakingBalance.Data.P2P {
fun convert(response: P2PEthPoolAccountResponse, source: StatusSource): StakingBalance.Data.P2PEthPool {
val stakingId = StakingID(
integrationId = StakingIntegrationID.P2P.EthereumPooled.value,
integrationId = StakingIntegrationID.P2PEthPool.value,
address = response.delegatorAddress,
)
val account = P2PStakingAccount(
val account = P2PEthPoolStakingAccount(
delegatorAddress = response.delegatorAddress,
vaultAddress = response.vaultAddress,
stake = convertStake(response.stake),
@ -27,29 +27,29 @@ internal object P2PStakingBalanceConverter {
exitQueue = convertExitQueue(response.exitQueue),
)
return StakingBalance.Data.P2P(
return StakingBalance.Data.P2PEthPool(
stakingId = stakingId,
source = source,
account = account,
)
}
private fun convertStake(dto: P2PEthPoolStakeDTO): P2PStake {
return P2PStake(
private fun convertStake(dto: P2PEthPoolStakeDTO): P2PEthPoolStake {
return P2PEthPoolStake(
assets = dto.assets,
totalEarnedAssets = dto.totalEarnedAssets,
)
}
private fun convertExitQueue(dto: P2PEthPoolExitQueueDTO): P2PExitQueue {
return P2PExitQueue(
private fun convertExitQueue(dto: P2PEthPoolExitQueueDTO): P2PEthPoolExitQueue {
return P2PEthPoolExitQueue(
total = dto.total.toBigDecimal(),
requests = dto.requests.map(::convertExitRequest),
)
}
private fun convertExitRequest(dto: P2PEthPoolExitRequestDTO): P2PExitRequest {
return P2PExitRequest(
private fun convertExitRequest(dto: P2PEthPoolExitRequestDTO): P2PEthPoolExitRequest {
return P2PEthPoolExitRequest(
ticket = dto.ticket,
totalAssets = dto.totalAssets.toBigDecimal(),
timestamp = Instant.fromEpochSeconds(dto.timestamp),

View file

@ -6,7 +6,7 @@ import com.tangem.utils.converter.Converter
import java.math.BigDecimal
/**
* Converter from P2P Unsigned Transaction DTO to Domain model
* Converter from P2PEthPool Unsigned Transaction DTO to Domain model
*/
internal object P2PEthPoolUnsignedTxConverter : Converter<P2PEthPoolUnsignedTxDTO, P2PEthPoolUnsignedTx> {

View file

@ -5,7 +5,7 @@ import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
import com.tangem.utils.converter.Converter
/**
* Converter from P2P Vault DTO to Domain model
* Converter from P2PEthPool Vault DTO to Domain model
*/
internal object P2PEthPoolVaultConverter : Converter<P2PEthPoolVaultDTO, P2PEthPoolVault> {

View file

@ -1,92 +0,0 @@
package com.tangem.data.staking.converters.ethpool
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.staking.*
import com.tangem.domain.staking.model.ethpool.P2PEthPoolAccount
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
import java.math.BigDecimal
/**
* tmp solution before facade implementation
*/
internal object P2PYieldBalanceConverter {
private const val ETH_DECIMALS = 18
private const val ETH_SYMBOL = "ETH"
private const val ETH_NAME = "Ethereum"
private const val ETH_COINGECKO_ID = "ethereum"
fun convert(
account: P2PEthPoolAccount,
vault: P2PEthPoolVault,
address: String,
source: StatusSource,
): YieldBalance {
val integrationId = "p2p-ethereum-pooled"
val stakingId = StakingID(
integrationId = integrationId,
address = address,
)
val balanceItems = buildBalanceItems(account, vault)
return if (balanceItems.isEmpty()) {
YieldBalance.Empty(stakingId = stakingId, source = source)
} else {
YieldBalance.Data(
stakingId = stakingId,
source = source,
balance = YieldBalanceItem(
items = balanceItems,
integrationId = integrationId,
),
)
}
}
private fun buildBalanceItems(account: P2PEthPoolAccount, vault: P2PEthPoolVault): List<BalanceItem> = buildList {
if (account.stake.assets > BigDecimal.ZERO) {
add(
createBalanceItem(
groupId = "p2p-staked",
amount = account.stake.assets,
type = BalanceType.STAKED,
validatorAddress = vault.vaultAddress,
),
)
}
}
private fun createBalanceItem(
groupId: String,
amount: BigDecimal,
type: BalanceType,
validatorAddress: String,
): BalanceItem {
return BalanceItem(
groupId = groupId,
token = createEthToken(),
type = type,
amount = amount,
rawCurrencyId = ETH_COINGECKO_ID,
validatorAddress = validatorAddress,
date = null,
pendingActions = emptyList(),
pendingActionsConstraints = emptyList(),
isPending = false,
)
}
private fun createEthToken(): YieldToken {
return YieldToken(
name = ETH_NAME,
network = NetworkType.ETHEREUM,
symbol = ETH_SYMBOL,
decimals = ETH_DECIMALS,
address = null,
coinGeckoId = ETH_COINGECKO_ID,
logoURI = null,
isPoints = false,
)
}
}

View file

@ -1,9 +1,9 @@
package com.tangem.data.staking.di
import androidx.datastore.core.DataStore
import com.tangem.data.staking.store.DefaultP2PBalancesStore
import com.tangem.data.staking.store.DefaultP2PEthPoolBalancesStore
import com.tangem.data.staking.store.DefaultStakingBalancesStore
import com.tangem.data.staking.store.P2PBalancesStore
import com.tangem.data.staking.store.P2PEthPoolBalancesStore
import com.tangem.data.staking.store.StakingBalancesStore
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
@ -38,11 +38,11 @@ internal object StakingBalanceSupplierModule {
@Provides
@Singleton
fun provideP2PBalancesStore(
fun provideP2PEthPoolBalancesStore(
persistenceStore: DataStore<Map<String, Set<P2PEthPoolAccountResponse>>>,
dispatchers: CoroutineDispatcherProvider,
): P2PBalancesStore {
return DefaultP2PBalancesStore(
): P2PEthPoolBalancesStore {
return DefaultP2PEthPoolBalancesStore(
runtimeStore = RuntimeSharedStore(),
persistenceStore = persistenceStore,
dispatchers = dispatchers,

View file

@ -75,12 +75,12 @@ internal object StakingDataModule {
@Provides
@Singleton
fun provideP2PEthPoolRepository(
p2pApi: P2PEthPoolApi,
p2pEthPoolApi: P2PEthPoolApi,
p2pEthPoolVaultsStore: P2PEthPoolVaultsStore,
dispatchers: CoroutineDispatcherProvider,
): P2PEthPoolRepository {
return DefaultP2PEthPoolRepository(
p2pApi = p2pApi,
p2pEthPoolApi = p2pEthPoolApi,
p2pEthPoolVaultsStore = p2pEthPoolVaultsStore,
dispatchers = dispatchers,
)

View file

@ -5,7 +5,7 @@ import arrow.core.left
import arrow.core.right
import arrow.core.toOption
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.staking.store.P2PBalancesStore
import com.tangem.data.staking.store.P2PEthPoolBalancesStore
import com.tangem.data.staking.store.StakingBalancesStore
import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory
import com.tangem.datasource.api.common.response.ApiResponse
@ -24,7 +24,8 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runSuspendCatching
@ -38,16 +39,16 @@ import javax.inject.Inject
/**
* Default implementation of [MultiStakingBalanceFetcher]
*
* Supports both StakeKit and P2P staking providers.
* Supports both StakeKit and P2PEthPool staking providers.
*
* @property userWalletsStore user wallets store
* @property stakingYieldsStore staking yields store
* @property stakingBalancesStore staking balances store (StakeKit)
* @property p2pBalancesStore P2P balances store
* @property stakeKitApi stake kit API
* @property p2pApi P2P ETH Pool API
* @property p2pVaultsStore P2P vaults store
* @property dispatchers dispatchers
* @property userWalletsStore user wallets store
* @property stakingYieldsStore staking yields store
* @property stakingBalancesStore staking balances store (StakeKit)
* @property p2PEthPoolBalancesStore P2PEthPool balances store
* @property stakeKitApi stake kit API
* @property p2pEthPoolApi P2PEthPool API
* @property p2pEthPoolVaultsStore P2PEthPool vaults store
* @property dispatchers dispatchers
*
[REDACTED_AUTHOR]
*/
@ -56,10 +57,10 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
private val userWalletsStore: UserWalletsStore,
private val stakingYieldsStore: StakingYieldsStore,
private val stakingBalancesStore: StakingBalancesStore,
private val p2pBalancesStore: P2PBalancesStore,
private val p2PEthPoolBalancesStore: P2PEthPoolBalancesStore,
private val stakeKitApi: StakeKitApi,
private val p2pApi: P2PEthPoolApi,
private val p2pVaultsStore: P2PEthPoolVaultsStore,
private val p2pEthPoolApi: P2PEthPoolApi,
private val p2pEthPoolVaultsStore: P2PEthPoolVaultsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : MultiStakingBalanceFetcher {
@ -75,7 +76,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
return it.left()
}
val (stakeKitIds, p2pIds) = stakingIds.partition { stakingId ->
val (stakeKitIds, p2pEthPoolIds) = stakingIds.partition { stakingId ->
val stakingIntegrationID = StakingIntegrationID.entries.find {
it.value == stakingId.integrationId
}
@ -86,7 +87,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
"""
Staking IDs to fetch:
- StakeKit: ${stakeKitIds.joinToString()}
- P2P: ${p2pIds.joinToString()}
- P2PEthPool: ${p2pEthPoolIds.joinToString()}
""".trimIndent(),
)
@ -96,8 +97,8 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
launch { fetchStakeKitBalances(params.userWalletId, stakeKitIds.toSet()) }
}
if (p2pIds.isNotEmpty()) {
launch { fetchP2PBalances(params.userWalletId, p2pIds.toSet()) }
if (p2pEthPoolIds.isNotEmpty()) {
launch { fetchP2PBalances(params.userWalletId, p2pEthPoolIds.toSet()) }
}
}
}
@ -110,8 +111,11 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
stakingIds = stakeKitIds.toSet(),
)
}
if (p2pIds.isNotEmpty()) {
p2pBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = p2pIds.toSet())
if (p2pEthPoolIds.isNotEmpty()) {
p2PEthPoolBalancesStore.storeError(
userWalletId = params.userWalletId,
stakingIds = p2pEthPoolIds.toSet(),
)
}
}
}
@ -128,12 +132,12 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
}
private suspend fun fetchP2PBalances(userWalletId: UserWalletId, stakingIds: Set<StakingID>) {
p2pBalancesStore.refresh(userWalletId = userWalletId, stakingIds = stakingIds)
p2PEthPoolBalancesStore.refresh(userWalletId = userWalletId, stakingIds = stakingIds)
val vaults = runSuspendCatching { p2pVaultsStore.getSync() }.getOrNull().orEmpty()
val vaults = runSuspendCatching { p2pEthPoolVaultsStore.getSync() }.getOrNull().orEmpty()
if (vaults.isEmpty()) {
Timber.w("No P2P vaults available for $userWalletId")
p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
Timber.w("No P2PEthPool vaults available for $userWalletId")
p2PEthPoolBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
return
}
@ -143,59 +147,17 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
private suspend fun fetchFromP2P(
userWalletId: UserWalletId,
stakingIds: Set<StakingID>,
vaults: List<com.tangem.domain.staking.model.ethpool.P2PEthPoolVault>,
vaults: List<P2PEthPoolVault>,
) {
safeApiCall(
call = {
val addresses = stakingIds.map { it.address }.toSet()
val responses = fetchP2PAccountResponses(vaults = vaults, addresses = addresses)
val responses = mutableSetOf<P2PEthPoolAccountResponse>()
for (vault in vaults) {
for (address in addresses) {
runSuspendCatching {
val response = p2pApi.getAccountInfo(
network = P2PStakingConfig.activeNetwork.value,
delegatorAddress = address,
vaultAddress = vault.vaultAddress,
)
when (response) {
is ApiResponse.Success -> {
val data = response.data
if (data.error != null) {
Timber.w(
"P2P API returned error for vault ${vault.vaultAddress}, " +
"address $address: ${data.error ?: "error"}",
)
} else {
val result = requireNotNull(data.result) {
"Result is null in successful response"
}
responses.add(result)
}
}
is ApiResponse.Error -> {
Timber.w(
response.cause,
"Failed to fetch P2P balance for vault ${vault.vaultAddress}, " +
"address $address",
)
}
}
}.onFailure { error ->
Timber.w(
error,
"Failed to fetch P2P balance for vault ${vault.vaultAddress}, address $address",
)
}
}
}
Timber.i("Successfully fetched ${responses.size} P2P balances for $userWalletId")
Timber.i("Successfully fetched ${responses.size} P2PEthPool balances for $userWalletId")
if (responses.isNotEmpty()) {
p2pBalancesStore.storeActual(userWalletId = userWalletId, values = responses)
p2PEthPoolBalancesStore.storeActual(userWalletId = userWalletId, values = responses)
val missingStakingIds = stakingIds.filter { stakingId ->
responses.none { response ->
@ -205,23 +167,76 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
if (missingStakingIds.isNotEmpty()) {
Timber.i("Missing responses for ${missingStakingIds.size} staking IDs: $missingStakingIds")
p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = missingStakingIds.toSet())
p2PEthPoolBalancesStore.storeError(
userWalletId = userWalletId,
stakingIds = missingStakingIds.toSet(),
)
}
} else {
Timber.i("No P2P responses received for $userWalletId")
p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
Timber.i("No P2PEthPool responses received for $userWalletId")
p2PEthPoolBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
}
},
onError = { throwable ->
Timber.e(throwable, "Unable to fetch P2P balances $userWalletId")
Timber.e(throwable, "Unable to fetch P2PEthPool balances $userWalletId")
p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
p2PEthPoolBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
throw throwable
},
)
}
private suspend fun fetchP2PAccountResponses(
vaults: List<P2PEthPoolVault>,
addresses: Set<String>,
): Set<P2PEthPoolAccountResponse> {
val responses = mutableSetOf<P2PEthPoolAccountResponse>()
for (vault in vaults) {
for (address in addresses) {
runSuspendCatching {
val response = p2pEthPoolApi.getAccountInfo(
network = P2PEthPoolStakingConfig.activeNetwork.value,
delegatorAddress = address,
vaultAddress = vault.vaultAddress,
)
when (response) {
is ApiResponse.Success -> {
val data = response.data
if (data.error != null) {
Timber.w(
"P2PEthPool API returned error for vault ${vault.vaultAddress}, " +
"address $address: ${data.error ?: "error"}",
)
} else {
val result = requireNotNull(data.result) {
"Result is null in successful response"
}
responses.add(result)
}
}
is ApiResponse.Error -> {
Timber.w(
response.cause,
"Failed to fetch P2PEthPool balance for vault ${vault.vaultAddress}, " +
"address $address",
)
}
}
}.onFailure { error ->
Timber.w(
error,
"Failed to fetch P2PEthPool balance for vault ${vault.vaultAddress}, address $address",
)
}
}
}
return responses
}
private inline fun checkIsSupportedByWalletOrElse(userWalletId: UserWalletId, ifNotSupported: (Throwable) -> Unit) {
val maybeUserWallet = userWalletsStore.getSyncOrNull(key = userWalletId).toOption()

View file

@ -2,7 +2,7 @@ package com.tangem.data.staking.multi
import arrow.core.Option
import arrow.core.some
import com.tangem.data.staking.store.P2PBalancesStore
import com.tangem.data.staking.store.P2PEthPoolBalancesStore
import com.tangem.data.staking.store.StakingBalancesStore
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.staking.multi.MultiStakingBalanceProducer
@ -19,11 +19,11 @@ import kotlinx.coroutines.flow.onEmpty
/**
* Default implementation of [MultiStakingBalanceProducer]
*
* Combines staking balances from both StakeKit and P2P providers.
* Combines staking balances from both StakeKit and P2PEthPool providers.
*
* @property params params
* @property stakingBalancesStore StakeKit staking balances store
* @property p2pBalancesStore P2P balances store
* @property p2PEthPoolBalancesStore P2PEthPool balances store
* @property dispatchers dispatchers
*
[REDACTED_AUTHOR]
@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.onEmpty
internal class DefaultMultiStakingBalanceProducer @AssistedInject constructor(
@Assisted val params: MultiStakingBalanceProducer.Params,
private val stakingBalancesStore: StakingBalancesStore,
private val p2pBalancesStore: P2PBalancesStore,
private val p2PEthPoolBalancesStore: P2PEthPoolBalancesStore,
private val dispatchers: CoroutineDispatcherProvider,
) : MultiStakingBalanceProducer {
@ -39,10 +39,10 @@ internal class DefaultMultiStakingBalanceProducer @AssistedInject constructor(
override fun produce(): Flow<Set<StakingBalance>> {
val stakeKitFlow = stakingBalancesStore.get(userWalletId = params.userWalletId)
val p2pFlow = p2pBalancesStore.get(userWalletId = params.userWalletId)
val p2pEthPoolFlow = p2PEthPoolBalancesStore.get(userWalletId = params.userWalletId)
return combine(stakeKitFlow, p2pFlow) { stakeKitBalances, p2pBalances ->
stakeKitBalances + p2pBalances
return combine(stakeKitFlow, p2pEthPoolFlow) { stakeKitBalances, p2pEthPoolBalances ->
stakeKitBalances + p2pEthPoolBalances
}
.distinctUntilChanged()
.onEmpty { emit(value = hashSetOf()) }

View file

@ -1,7 +1,7 @@
package com.tangem.data.staking.store
import androidx.datastore.core.DataStore
import com.tangem.data.staking.converters.ethpool.P2PStakingBalanceConverter
import com.tangem.data.staking.converters.ethpool.P2PEthPoolStakingBalanceConverter
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.domain.models.StatusSource
@ -20,22 +20,22 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
internal typealias WalletIdWithP2PStakingBalances = Map<UserWalletId, Set<StakingBalance>>
internal typealias WalletIdWithP2PResponses = Map<String, Set<P2PEthPoolAccountResponse>>
internal typealias WalletIdWithP2PEthPoolResponses = Map<String, Set<P2PEthPoolAccountResponse>>
/**
* Default implementation of [P2PBalancesStore]
* Default implementation of [P2PEthPoolBalancesStore]
*
* Stores P2P ETH Pool staking balances.
* Stores P2PEthPool staking balances.
*
* @property runtimeStore runtime store
* @property persistenceStore persistence store
* @param dispatchers coroutine dispatchers
*/
internal class DefaultP2PBalancesStore(
internal class DefaultP2PEthPoolBalancesStore(
private val runtimeStore: RuntimeSharedStore<WalletIdWithP2PStakingBalances>,
private val persistenceStore: DataStore<WalletIdWithP2PResponses>,
private val persistenceStore: DataStore<WalletIdWithP2PEthPoolResponses>,
dispatchers: CoroutineDispatcherProvider,
) : P2PBalancesStore {
) : P2PEthPoolBalancesStore {
private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io)
@ -47,7 +47,7 @@ internal class DefaultP2PBalancesStore(
value = cachedData.map { (stringWalletId, responses) ->
val key = UserWalletId(stringWalletId)
val value = responses.map { response ->
P2PStakingBalanceConverter.convert(
P2PEthPoolStakingBalanceConverter.convert(
response = response,
source = StatusSource.CACHE,
)
@ -108,7 +108,7 @@ internal class DefaultP2PBalancesStore(
private suspend fun storeInRuntime(userWalletId: UserWalletId, values: Set<P2PEthPoolAccountResponse>) {
val newBalances = values.map { response ->
P2PStakingBalanceConverter.convert(
P2PEthPoolStakingBalanceConverter.convert(
response = response,
source = StatusSource.ACTUAL,
)
@ -152,7 +152,7 @@ internal class DefaultP2PBalancesStore(
current.toMutableMap().apply {
this[userWalletId.stringValue] = this[userWalletId.stringValue].orEmpty()
.filterNot { response ->
StakingIntegrationID.P2P.EthereumPooled.value in integrationIds
StakingIntegrationID.P2PEthPool.value in integrationIds
}
.toSet()
}

View file

@ -7,9 +7,9 @@ import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
/**
* Store for P2P ETH Pool staking balances
* Store for P2PEthPool staking balances
*/
interface P2PBalancesStore {
interface P2PEthPoolBalancesStore {
fun get(userWalletId: UserWalletId): Flow<Set<StakingBalance>>

View file

@ -1,6 +1,6 @@
package com.tangem.data.staking
import com.tangem.data.staking.converters.ethpool.P2PStakingBalanceConverter
import com.tangem.data.staking.converters.ethpool.P2PEthPoolStakingBalanceConverter
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.datasource.local.token.converter.StakingBalanceConverter
@ -11,8 +11,10 @@ internal fun YieldBalanceWrapperDTO.toDomain(source: StatusSource = StatusSource
return StakingBalanceConverter(isCached = source == StatusSource.CACHE).convert(this)!!
}
internal fun P2PEthPoolAccountResponse.toDomain(source: StatusSource = StatusSource.CACHE): StakingBalance.Data.P2P {
return P2PStakingBalanceConverter.convert(
internal fun P2PEthPoolAccountResponse.toDomain(
source: StatusSource = StatusSource.CACHE,
): StakingBalance.Data.P2PEthPool {
return P2PEthPoolStakingBalanceConverter.convert(
response = this,
source = source,
)

View file

@ -4,7 +4,7 @@ import arrow.core.toOption
import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory
import com.tangem.common.test.data.staking.MockYieldDTOFactory
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
import com.tangem.data.staking.store.P2PBalancesStore
import com.tangem.data.staking.store.P2PEthPoolBalancesStore
import com.tangem.data.staking.store.StakingBalancesStore
import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory
import com.tangem.datasource.api.common.response.ApiResponse
@ -36,19 +36,19 @@ internal class DefaultMultiStakingBalanceFetcherTest {
private val userWalletsStore: UserWalletsStore = mockk()
private val stakingYieldsStore: StakingYieldsStore = mockk()
private val stakingBalancesStore: StakingBalancesStore = mockk(relaxUnitFun = true)
private val p2pBalancesStore: P2PBalancesStore = mockk(relaxUnitFun = true)
private val p2PEthPoolBalancesStore: P2PEthPoolBalancesStore = mockk(relaxUnitFun = true)
private val stakeKitApi: StakeKitApi = mockk()
private val p2pApi: P2PEthPoolApi = mockk()
private val p2pVaultsStore: P2PEthPoolVaultsStore = mockk()
private val p2pEthPoolApi: P2PEthPoolApi = mockk()
private val p2pEthPoolVaultsStore: P2PEthPoolVaultsStore = mockk()
private val fetcher = DefaultMultiStakingBalanceFetcher(
userWalletsStore = userWalletsStore,
stakingYieldsStore = stakingYieldsStore,
stakingBalancesStore = stakingBalancesStore,
p2pBalancesStore = p2pBalancesStore,
p2PEthPoolBalancesStore = p2PEthPoolBalancesStore,
stakeKitApi = stakeKitApi,
p2pApi = p2pApi,
p2pVaultsStore = p2pVaultsStore,
p2pEthPoolApi = p2pEthPoolApi,
p2pEthPoolVaultsStore = p2pEthPoolVaultsStore,
dispatchers = TestingCoroutineDispatcherProvider(),
)

View file

@ -3,7 +3,7 @@ package com.tangem.data.staking.multi
import com.google.common.truth.Truth
import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory
import com.tangem.common.test.data.staking.MockP2PEthPoolAccountResponseFactory
import com.tangem.data.staking.store.P2PBalancesStore
import com.tangem.data.staking.store.P2PEthPoolBalancesStore
import com.tangem.data.staking.store.StakingBalancesStore
import com.tangem.data.staking.toDomain
import com.tangem.domain.models.StatusSource
@ -28,13 +28,13 @@ internal class DefaultMultiStakingBalanceProducerTest {
private val params = MultiStakingBalanceProducer.Params(userWalletId = UserWalletId("011"))
private val stakingBalancesStore = mockk<StakingBalancesStore>()
private val p2pBalancesStore = mockk<P2PBalancesStore>()
private val p2PEthPoolBalancesStore = mockk<P2PEthPoolBalancesStore>()
private val dispatchers = TestingCoroutineDispatcherProvider()
private val producer = DefaultMultiStakingBalanceProducer(
params = params,
stakingBalancesStore = stakingBalancesStore,
p2pBalancesStore = p2pBalancesStore,
p2PEthPoolBalancesStore = p2PEthPoolBalancesStore,
dispatchers = dispatchers,
)
@ -48,13 +48,13 @@ internal class DefaultMultiStakingBalanceProducerTest {
val networksStatusesFlow = flowOf(balances)
every { stakingBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
every { p2PEthPoolBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
val actual = producer.produce()
// check after producer.produce()
verify { stakingBalancesStore.get(params.userWalletId) }
verify { p2pBalancesStore.get(params.userWalletId) }
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
val values = getEmittedValues(flow = actual)
@ -67,13 +67,13 @@ internal class DefaultMultiStakingBalanceProducerTest {
val networksStatusesFlow = MutableSharedFlow<Set<StakingBalance>>(replay = 2)
every { stakingBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
every { p2PEthPoolBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
val actual = producer.produce()
// check after producer.produce()
verify { stakingBalancesStore.get(params.userWalletId) }
verify { p2pBalancesStore.get(params.userWalletId) }
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
// first emit
val balances = setOf(
@ -108,13 +108,13 @@ internal class DefaultMultiStakingBalanceProducerTest {
val networksStatusesFlow = MutableSharedFlow<Set<StakingBalance>>(replay = 2)
every { stakingBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
every { p2PEthPoolBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
val actual = producer.produce()
// check after producer.produce()
verify { stakingBalancesStore.get(params.userWalletId) }
verify { p2pBalancesStore.get(params.userWalletId) }
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
// first emit
val wrappers = setOf(
@ -157,13 +157,13 @@ internal class DefaultMultiStakingBalanceProducerTest {
.buffer(capacity = 5)
every { stakingBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
every { p2PEthPoolBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
val actual = producer.produceWithFallback()
// check after producer.produce()
verify { stakingBalancesStore.get(params.userWalletId) }
verify { p2pBalancesStore.get(params.userWalletId) }
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
val values1 = getEmittedValues(flow = actual)
@ -181,13 +181,13 @@ internal class DefaultMultiStakingBalanceProducerTest {
@Test
fun `test that flow is empty`() = runTest {
every { stakingBalancesStore.get(params.userWalletId) } returns emptyFlow()
every { p2pBalancesStore.get(params.userWalletId) } returns emptyFlow()
every { p2PEthPoolBalancesStore.get(params.userWalletId) } returns emptyFlow()
val actual = producer.produce()
// check after producer.produce()
verify { stakingBalancesStore.get(params.userWalletId) }
verify { p2pBalancesStore.get(params.userWalletId) }
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
val values = getEmittedValues(flow = actual)
@ -198,53 +198,53 @@ internal class DefaultMultiStakingBalanceProducerTest {
@Test
fun `test that StakeKit and P2P balances are combined`() = runTest {
val stakeKitBalances = createStakeKitBalances()
val p2pBalances = createP2PBalances()
val p2pEthPoolBalances = createP2PEthPoolBalances()
every { stakingBalancesStore.get(params.userWalletId) } returns flowOf(stakeKitBalances)
every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(p2pBalances)
every { p2PEthPoolBalancesStore.get(params.userWalletId) } returns flowOf(p2pEthPoolBalances)
val actual = producer.produce()
// check after producer.produce()
verify { stakingBalancesStore.get(params.userWalletId) }
verify { p2pBalancesStore.get(params.userWalletId) }
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
val values = getEmittedValues(flow = actual)
Truth.assertThat(values.size).isEqualTo(1)
Truth.assertThat(values.first()).isEqualTo(stakeKitBalances + p2pBalances)
Truth.assertThat(values.first()).isEqualTo(stakeKitBalances + p2pEthPoolBalances)
}
@Test
fun `test that P2P balances are updated independently from StakeKit`() = runTest {
val stakeKitBalances = createStakeKitBalancesWithTonOnly()
val p2pFlow = MutableSharedFlow<Set<StakingBalance>>(replay = 2)
val p2pEthPoolFlow = MutableSharedFlow<Set<StakingBalance>>(replay = 2)
every { stakingBalancesStore.get(params.userWalletId) } returns flowOf(stakeKitBalances)
every { p2pBalancesStore.get(params.userWalletId) } returns p2pFlow
every { p2PEthPoolBalancesStore.get(params.userWalletId) } returns p2pEthPoolFlow
val actual = producer.produce()
// check after producer.produce()
verify { stakingBalancesStore.get(params.userWalletId) }
verify { p2pBalancesStore.get(params.userWalletId) }
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
// first emit - empty P2P
p2pFlow.emit(emptySet())
// first emit - empty P2PEthPool
p2pEthPoolFlow.emit(emptySet())
val values1 = getEmittedValues(flow = actual)
Truth.assertThat(values1.size).isEqualTo(1)
Truth.assertThat(values1.first()).isEqualTo(stakeKitBalances)
// second emit - with P2P balance
val p2pBalances = createP2PBalances()
p2pFlow.emit(p2pBalances)
// second emit - with P2PEthPool balance
val p2pEthPoolBalances = createP2PEthPoolBalances()
p2pEthPoolFlow.emit(p2pEthPoolBalances)
val values2 = getEmittedValues(flow = actual)
Truth.assertThat(values2.size).isEqualTo(2)
Truth.assertThat(values2.last()).isEqualTo(stakeKitBalances + p2pBalances)
Truth.assertThat(values2.last()).isEqualTo(stakeKitBalances + p2pEthPoolBalances)
}
private companion object {
@ -255,7 +255,7 @@ internal class DefaultMultiStakingBalanceProducerTest {
address = "0x1",
)
val p2pEthereumId = StakingID(
integrationId = StakingIntegrationID.P2P.EthereumPooled.value,
integrationId = StakingIntegrationID.P2PEthPool.value,
address = "0x5aa711F440Eb6d4361148bBD89d03464628ace84",
)
@ -272,7 +272,7 @@ internal class DefaultMultiStakingBalanceProducerTest {
)
}
fun createP2PBalances(): Set<StakingBalance> {
fun createP2PEthPoolBalances(): Set<StakingBalance> {
return setOf(
MockP2PEthPoolAccountResponseFactory.createWithBalance(stakingId = p2pEthereumId).toDomain(
source = StatusSource.ACTUAL,