Updated on 2026-08-14
This commit is contained in:
parent
9e0b8b3234
commit
b449775ad8
112 changed files with 979 additions and 740 deletions
|
|
@ -100,12 +100,12 @@ internal object StakingDomainModule {
|
|||
@Singleton
|
||||
fun provideFetchStakingOptionsUseCase(
|
||||
stakeKitRepository: StakeKitRepository,
|
||||
p2pRepository: P2PEthPoolRepository,
|
||||
p2pEthPoolRepository: P2PEthPoolRepository,
|
||||
stakingErrorResolver: StakingErrorResolver,
|
||||
): FetchStakingOptionsUseCase {
|
||||
return FetchStakingOptionsUseCase(
|
||||
stakeKitRepository = stakeKitRepository,
|
||||
p2pRepository = p2pRepository,
|
||||
p2pEthPoolRepository = p2pEthPoolRepository,
|
||||
stakingErrorResolver = stakingErrorResolver,
|
||||
)
|
||||
}
|
||||
|
|
@ -240,8 +240,9 @@ internal object StakingDomainModule {
|
|||
@Singleton
|
||||
fun provideStakingApyFlowUseCase(
|
||||
stakeKitRepository: StakeKitRepository,
|
||||
p2pEthPoolRepository: P2PEthPoolRepository,
|
||||
stakingFeatureToggles: StakingFeatureToggles,
|
||||
): StakingApyFlowUseCase {
|
||||
return StakingApyFlowUseCase(stakeKitRepository, stakingFeatureToggles)
|
||||
return StakingApyFlowUseCase(stakeKitRepository, p2pEthPoolRepository, stakingFeatureToggles)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.tap.network.auth
|
||||
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
||||
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
|
||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||
|
||||
internal class DefaultP2PEthPoolAuthProvider(
|
||||
|
|
@ -12,6 +12,6 @@ internal class DefaultP2PEthPoolAuthProvider(
|
|||
val keys = environmentConfigStorage.getConfigSync().p2pApiKey
|
||||
?: error("No P2P api keys provided")
|
||||
|
||||
return if (P2PStakingConfig.USE_TESTNET) keys.hoodi else keys.mainnet
|
||||
return if (P2PEthPoolStakingConfig.USE_TESTNET) keys.hoodi else keys.mainnet
|
||||
}
|
||||
}
|
||||
|
|
@ -311,7 +311,7 @@ internal class ChildFactory @Inject constructor(
|
|||
params = StakingComponent.Params(
|
||||
userWalletId = route.userWalletId,
|
||||
cryptoCurrency = route.cryptoCurrency,
|
||||
yieldId = route.yieldId,
|
||||
integrationId = route.integrationId,
|
||||
),
|
||||
componentFactory = stakingComponentFactory,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ dependencies {
|
|||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.staking.models)
|
||||
implementation(projects.domain.staking)
|
||||
implementation(projects.domain.markets.models)
|
||||
implementation(projects.domain.onramp.models)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import android.os.Bundle
|
|||
import com.tangem.common.routing.bundle.RouteBundleParams
|
||||
import com.tangem.common.routing.bundle.bundle
|
||||
import com.tangem.common.routing.entity.InitScreenLaunchMode
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.feedback.models.WalletMetaInfo
|
||||
|
|
@ -208,8 +209,8 @@ sealed class AppRoute(val path: String) : Route {
|
|||
data class Staking(
|
||||
val userWalletId: UserWalletId,
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
val yieldId: String,
|
||||
) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrency.id.value}/$yieldId")
|
||||
val integrationId: StakingIntegrationID,
|
||||
) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrency.id.value}/${integrationId.value}")
|
||||
|
||||
@Serializable
|
||||
data class PushNotification(
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
|||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Factory for creating mock P2P ETH Pool account responses for testing
|
||||
* Factory for creating mock P2PEthPool account responses for testing
|
||||
*/
|
||||
object MockP2PEthPoolAccountResponseFactory {
|
||||
|
||||
|
|
|
|||
|
|
@ -22,8 +22,10 @@ import com.tangem.domain.models.currency.CryptoCurrency
|
|||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.currency.yieldSupplyKey
|
||||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.staking.model.StakingTarget
|
||||
import com.tangem.domain.staking.model.isStakingSupported
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.common.RewardInfo
|
||||
import com.tangem.domain.staking.model.common.RewardType
|
||||
import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.StringsSigns.DASH_SIGN
|
||||
|
|
@ -44,7 +46,7 @@ import java.math.BigDecimal
|
|||
class TokenItemStateConverter(
|
||||
private val appCurrency: AppCurrency,
|
||||
private val yieldModuleApyMap: Map<String, BigDecimal> = emptyMap(),
|
||||
private val stakingApyMap: Map<String, List<Yield.Validator>> = emptyMap(),
|
||||
private val stakingApyMap: Map<String, List<StakingTarget>> = emptyMap(),
|
||||
private val yieldSupplyPromoBannerKey: String? = null,
|
||||
private val iconStateProvider: (CryptoCurrencyStatus) -> CurrencyIconState = {
|
||||
CryptoCurrencyToIconStateConverter().convert(it)
|
||||
|
|
@ -177,7 +179,7 @@ class TokenItemStateConverter(
|
|||
private fun createTitleState(
|
||||
currencyStatus: CryptoCurrencyStatus,
|
||||
yieldModuleApyMap: Map<String, BigDecimal>,
|
||||
stakingApyMap: Map<String, List<Yield.Validator>>,
|
||||
stakingApyMap: Map<String, List<StakingTarget>>,
|
||||
onApyLabelClick: ((CryptoCurrencyStatus, ApySource, String) -> Unit)?,
|
||||
): TokenItemState.TitleState {
|
||||
return when (val value = currencyStatus.value) {
|
||||
|
|
@ -217,7 +219,7 @@ class TokenItemStateConverter(
|
|||
private fun resolveEarnApy(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
yieldModuleApyMap: Map<String, BigDecimal>,
|
||||
stakingApyMap: Map<String, List<Yield.Validator>>,
|
||||
stakingApyMap: Map<String, List<StakingTarget>>,
|
||||
): EarnApyInfo? {
|
||||
val token = cryptoCurrencyStatus.currency as? CryptoCurrency.Token
|
||||
if (token != null && yieldModuleApyMap.isNotEmpty()) {
|
||||
|
|
@ -247,9 +249,9 @@ class TokenItemStateConverter(
|
|||
stakingApyMap = stakingApyMap,
|
||||
)
|
||||
val rewardTypeRes = when (stakingInfo.rewardType) {
|
||||
Yield.RewardType.APR -> R.string.staking_apr_earn_badge
|
||||
Yield.RewardType.UNKNOWN,
|
||||
Yield.RewardType.APY,
|
||||
RewardType.APR -> R.string.staking_apr_earn_badge
|
||||
RewardType.UNKNOWN,
|
||||
RewardType.APY,
|
||||
null,
|
||||
-> R.string.yield_module_earn_badge
|
||||
}
|
||||
|
|
@ -272,49 +274,35 @@ class TokenItemStateConverter(
|
|||
|
||||
private fun findStakingRate(
|
||||
currencyStatus: CryptoCurrencyStatus,
|
||||
stakingApyMap: Map<String, List<Yield.Validator>>,
|
||||
stakingApyMap: Map<String, List<StakingTarget>>,
|
||||
): StakingLocalInfo {
|
||||
val stakingKey = currencyStatus.currency.stakingKey()
|
||||
val validators = stakingApyMap[stakingKey]
|
||||
val targets = stakingApyMap[stakingKey]
|
||||
?: return StakingLocalInfo(rate = null, isActive = false, rewardType = null)
|
||||
|
||||
val stakingBalance = currencyStatus.value.stakingBalance as? StakingBalance.Data
|
||||
val stakeKitBalance = stakingBalance as? StakingBalance.Data.StakeKit
|
||||
|
||||
val rateInfo: Pair<BigDecimal, Yield.RewardType?>? = if (stakeKitBalance != null) {
|
||||
val rewardInfo: RewardInfo? = if (stakeKitBalance != null) {
|
||||
// StakeKit-specific: try to find rate from validator address
|
||||
val validatorsByAddress = validators.associateBy { it.address }
|
||||
val targetsByAddress = targets.associateBy { it.address }
|
||||
stakeKitBalance.balance.items
|
||||
.mapNotNull { it.validatorAddress }
|
||||
.firstNotNullOfOrNull { address ->
|
||||
val validator = validatorsByAddress[address]
|
||||
validator?.rewardInfo?.rate?.let { rate ->
|
||||
rate to validator.rewardInfo?.type
|
||||
}
|
||||
}
|
||||
?: validators
|
||||
.filter { it.preferred }
|
||||
.mapNotNull { validator ->
|
||||
validator.rewardInfo?.rate?.let { rate -> rate to validator.rewardInfo?.type }
|
||||
}
|
||||
.maxByOrNull { it.first }
|
||||
.firstNotNullOfOrNull { address -> targetsByAddress[address]?.rewardInfo }
|
||||
?: targets
|
||||
.filter { it.isPreferred }
|
||||
.mapNotNull { it.rewardInfo }
|
||||
.maxByOrNull { it.rate }
|
||||
} else {
|
||||
// P2P or no balance: use preferred validators
|
||||
// TODO p2p
|
||||
validators
|
||||
.filter { it.preferred }
|
||||
.mapNotNull { validator ->
|
||||
validator.rewardInfo?.rate?.let { rate ->
|
||||
rate to validator.rewardInfo?.type
|
||||
}
|
||||
}
|
||||
.maxByOrNull { it.first }
|
||||
targets
|
||||
.mapNotNull { it.rewardInfo }
|
||||
.maxByOrNull { it.rate }
|
||||
}
|
||||
|
||||
return StakingLocalInfo(
|
||||
rate = rateInfo?.first,
|
||||
rate = rewardInfo?.rate,
|
||||
isActive = stakingBalance != null,
|
||||
rewardType = rateInfo?.second,
|
||||
rewardType = rewardInfo?.type,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -470,7 +458,7 @@ class TokenItemStateConverter(
|
|||
private data class StakingLocalInfo(
|
||||
val rate: BigDecimal?,
|
||||
val isActive: Boolean,
|
||||
val rewardType: Yield.RewardType?,
|
||||
val rewardType: RewardType?,
|
||||
)
|
||||
|
||||
private data class EarnApyInfo(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
|
||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ internal class P2PEthPool(
|
|||
private fun getInitialEnvironment(): ApiEnvironment {
|
||||
return when (BuildConfig.BUILD_TYPE) {
|
||||
MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK
|
||||
else -> if (P2PStakingConfig.USE_TESTNET) ApiEnvironment.DEV else ApiEnvironment.PROD
|
||||
else -> if (P2PEthPoolStakingConfig.USE_TESTNET) ApiEnvironment.DEV else ApiEnvironment.PROD
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ data class P2PEthPoolBroadcastResponse(
|
|||
)
|
||||
|
||||
/**
|
||||
* Transaction status from P2P API
|
||||
* Transaction status from P2PEthPool API
|
||||
*/
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class P2PEthPoolTxStatusDTO {
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import com.squareup.moshi.Json
|
|||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Error response structure for P2P.org API
|
||||
* Error response structure for P2P.org eth pooled API
|
||||
*
|
||||
* All P2P API endpoints return errors in this format
|
||||
* All P2PEthPool API endpoints return errors in this format
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolErrorResponse(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import com.squareup.moshi.JsonClass
|
|||
/**
|
||||
* Unified response wrapper for all P2P.org API responses
|
||||
*
|
||||
* All P2P API endpoints return responses in this format:
|
||||
* All P2PEthPool API endpoints return responses in this format:
|
||||
* ```json
|
||||
* {
|
||||
* "error": null | { code, message, name, errors },
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ data class P2PEthPoolVaultsResponse(
|
|||
)
|
||||
|
||||
/**
|
||||
* Network identifier in P2P API
|
||||
* Network identifier in P2PEthPool API
|
||||
*/
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class P2PEthPoolNetworkDTO {
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ internal object StakingStoreModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideP2PBalancesPersistenceStore(
|
||||
fun provideP2PEthPoolBalancesPersistenceStore(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -91,7 +91,7 @@ internal object StakingStoreModule {
|
|||
types = mapWithStringKeyTypes(valueTypes = setTypes<P2PEthPoolAccountResponse>()),
|
||||
defaultValue = emptyMap(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = "p2p_balances") },
|
||||
produceFile = { context.dataStoreFile(fileName = "p2p_eth_pool_balances") },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.Flow
|
|||
* (similar to StakingYieldsStore for StakeKit yields)
|
||||
*
|
||||
* Vault is ETH-specific concept for pooled staking.
|
||||
* For other blockchains, P2P may use different structures.
|
||||
* For other blockchains, P2PEthPool may use different structures.
|
||||
*/
|
||||
interface P2PEthPoolVaultsStore {
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ interface P2PEthPoolVaultsStore {
|
|||
suspend fun getSync(): List<P2PEthPoolVault>
|
||||
|
||||
/**
|
||||
* Store vaults from P2P API
|
||||
* Store vaults from P2PEthPool API
|
||||
*/
|
||||
suspend fun store(vaults: List<P2PEthPoolVault>)
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_
|
|||
import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE
|
||||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.BLOCK_AID_API_KEY
|
||||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_API_KEY
|
||||
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
|
||||
import com.tangem.lib.auth.ExpressAuthProvider
|
||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||
import com.tangem.lib.auth.StakeKitAuthProvider
|
||||
|
|
@ -300,7 +300,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
}
|
||||
|
||||
private fun createP2PModel(): TestModel {
|
||||
val (environment, baseUrl) = if (P2PStakingConfig.USE_TESTNET) {
|
||||
val (environment, baseUrl) = if (P2PEthPoolStakingConfig.USE_TESTNET) {
|
||||
ApiEnvironment.DEV to "https://api-test.p2p.org/"
|
||||
} else {
|
||||
ApiEnvironment.PROD to "https://api.p2p.org/"
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
@ -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> {
|
||||
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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()) }
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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>>
|
||||
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -4,31 +4,31 @@ import com.tangem.domain.models.serialization.SerializedBigDecimal
|
|||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/** P2P.org staking account */
|
||||
/** P2P.org eth pooled staking account */
|
||||
@Serializable
|
||||
data class P2PStakingAccount(
|
||||
data class P2PEthPoolStakingAccount(
|
||||
val delegatorAddress: String,
|
||||
val vaultAddress: String,
|
||||
val stake: P2PStake,
|
||||
val stake: P2PEthPoolStake,
|
||||
val availableToUnstake: SerializedBigDecimal,
|
||||
val availableToWithdraw: SerializedBigDecimal,
|
||||
val exitQueue: P2PExitQueue,
|
||||
val exitQueue: P2PEthPoolExitQueue,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class P2PStake(
|
||||
data class P2PEthPoolStake(
|
||||
val assets: SerializedBigDecimal,
|
||||
val totalEarnedAssets: SerializedBigDecimal,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class P2PExitQueue(
|
||||
data class P2PEthPoolExitQueue(
|
||||
val total: SerializedBigDecimal,
|
||||
val requests: List<P2PExitRequest>,
|
||||
val requests: List<P2PEthPoolExitRequest>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class P2PExitRequest(
|
||||
data class P2PEthPoolExitRequest(
|
||||
val ticket: String,
|
||||
val totalAssets: SerializedBigDecimal,
|
||||
val timestamp: Instant,
|
||||
|
|
@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable
|
|||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Staking balance facade covering StakeKit and P2P balances
|
||||
* Staking balance facade covering StakeKit and P2PEthPool balances
|
||||
*/
|
||||
@Serializable
|
||||
sealed interface StakingBalance {
|
||||
|
|
@ -50,10 +50,10 @@ sealed interface StakingBalance {
|
|||
}
|
||||
|
||||
@Serializable
|
||||
data class P2P(
|
||||
data class P2PEthPool(
|
||||
override val stakingId: StakingID,
|
||||
override val source: StatusSource,
|
||||
val account: P2PStakingAccount,
|
||||
val account: P2PEthPoolStakingAccount,
|
||||
) : Data {
|
||||
|
||||
override val totalStaked: BigDecimal
|
||||
|
|
@ -93,7 +93,7 @@ sealed interface StakingBalance {
|
|||
fun copySealed(source: StatusSource): StakingBalance {
|
||||
return when (this) {
|
||||
is Data.StakeKit -> copy(source = source)
|
||||
is Data.P2P -> copy(source = source)
|
||||
is Data.P2PEthPool -> copy(source = source)
|
||||
is Empty -> copy(source = source)
|
||||
is Error -> this
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,4 +12,17 @@ data class YieldToken(
|
|||
val coinGeckoId: String?,
|
||||
val logoURI: String?,
|
||||
val isPoints: Boolean?,
|
||||
)
|
||||
) {
|
||||
companion object {
|
||||
val ETH = YieldToken( // TODO p2p
|
||||
name = "Ethereum",
|
||||
network = NetworkType.ETHEREUM,
|
||||
symbol = "ETH",
|
||||
decimals = 18,
|
||||
address = null,
|
||||
coinGeckoId = "ethereum",
|
||||
logoURI = null,
|
||||
isPoints = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -29,8 +29,6 @@ dependencies {
|
|||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
implementation(projects.features.staking.api)
|
||||
|
||||
implementation(tangemDeps.blockchain) {
|
||||
exclude(module = "joda-time")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
package com.tangem.domain.staking.model
|
||||
|
||||
import com.tangem.domain.staking.model.common.RewardInfo
|
||||
import com.tangem.domain.staking.model.common.RewardType
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
|
||||
/**
|
||||
* Represents either a StakeKit Validator or a P2P ETH Pool Vault.
|
||||
*/
|
||||
sealed interface StakingTarget {
|
||||
|
||||
/** Unique identifier (validator address or vault address) */
|
||||
val address: String
|
||||
|
||||
/** Display name */
|
||||
val name: String
|
||||
|
||||
/** Reward info (rate and type) */
|
||||
val rewardInfo: RewardInfo?
|
||||
|
||||
/** Whether this target is preferred/recommended */
|
||||
val isPreferred: Boolean
|
||||
|
||||
/** Whether this target is active and available for staking */
|
||||
val isActive: Boolean
|
||||
|
||||
/** Image URL for display (validator logo or vault icon) */
|
||||
val image: String?
|
||||
|
||||
/** Whether this is a strategic partner (shows special badge in UI) */
|
||||
val isStrategicPartner: Boolean
|
||||
|
||||
/**
|
||||
* StakeKit Validator wrapper
|
||||
*/
|
||||
data class Validator(val delegate: Yield.Validator) : StakingTarget {
|
||||
override val address: String = delegate.address
|
||||
override val name: String = delegate.name
|
||||
override val rewardInfo: RewardInfo? = delegate.rewardInfo
|
||||
override val isPreferred: Boolean = delegate.preferred
|
||||
override val isActive: Boolean = delegate.status == Yield.Validator.ValidatorStatus.ACTIVE
|
||||
override val image: String? = delegate.image
|
||||
override val isStrategicPartner: Boolean = delegate.isStrategicPartner
|
||||
}
|
||||
|
||||
/**
|
||||
* P2P ETH Pool Vault wrapper
|
||||
*/
|
||||
data class Vault(val vault: P2PEthPoolVault) : StakingTarget {
|
||||
override val address: String = vault.vaultAddress
|
||||
override val name: String = vault.displayName
|
||||
override val rewardInfo = RewardInfo(
|
||||
rate = vault.apy,
|
||||
type = RewardType.APY,
|
||||
)
|
||||
override val isPreferred: Boolean = true
|
||||
override val isActive: Boolean = true
|
||||
override val image: String? = null
|
||||
override val isStrategicPartner: Boolean = true
|
||||
}
|
||||
}
|
||||
|
||||
fun Yield.Validator.toStakingTarget(): StakingTarget = StakingTarget.Validator(this)
|
||||
|
||||
fun P2PEthPoolVault.toStakingTarget(): StakingTarget = StakingTarget.Vault(this)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.domain.staking.model.common
|
||||
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class RewardInfo(
|
||||
val rate: SerializedBigDecimal,
|
||||
val type: RewardType,
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.domain.staking.model.common
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
enum class RewardType {
|
||||
APY, // compound rate
|
||||
APR, // simple rate
|
||||
UNKNOWN,
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import com.tangem.domain.models.serialization.SerializedBigDecimal
|
|||
import org.joda.time.Instant
|
||||
|
||||
/**
|
||||
* P2P.org account staking information
|
||||
* Account staking information
|
||||
* Contains detailed balance and exit queue information
|
||||
*/
|
||||
data class P2PEthPoolAccount(
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ data class P2PEthPoolAction(
|
|||
)
|
||||
|
||||
/**
|
||||
* Types of P2P staking actions
|
||||
* Types of P2PEthPool staking actions
|
||||
*/
|
||||
@Serializable
|
||||
enum class P2PEthPoolActionType {
|
||||
|
|
@ -33,7 +33,7 @@ enum class P2PEthPoolActionType {
|
|||
}
|
||||
|
||||
/**
|
||||
* Status of P2P staking action
|
||||
* Status of P2PEthPool staking action
|
||||
*/
|
||||
@Serializable
|
||||
enum class P2PEthPoolActionStatus {
|
||||
|
|
@ -46,7 +46,7 @@ enum class P2PEthPoolActionStatus {
|
|||
}
|
||||
|
||||
/**
|
||||
* Transaction details for P2P staking action
|
||||
* Transaction details for P2PEthPool staking action
|
||||
*/
|
||||
data class P2PEthPoolStakingTransaction(
|
||||
val id: String?,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.tangem.domain.models.serialization.SerializedBigDecimal
|
|||
import org.joda.time.Instant
|
||||
|
||||
/**
|
||||
* P2P.org staking balance information (similar to StakeKit's YieldBalanceItem)
|
||||
* Staking balance information (similar to StakeKit's YieldBalanceItem)
|
||||
* Contains staked amounts, rewards, and pending actions
|
||||
*/
|
||||
data class P2PEthPoolStakingBalance(
|
||||
|
|
@ -15,7 +15,7 @@ data class P2PEthPoolStakingBalance(
|
|||
)
|
||||
|
||||
/**
|
||||
* Individual balance item for P2P staking
|
||||
* Individual balance item for P2PEthPool staking
|
||||
*/
|
||||
data class P2PEthPoolBalanceItem(
|
||||
val type: P2PEthPoolBalanceType,
|
||||
|
|
@ -26,7 +26,7 @@ data class P2PEthPoolBalanceItem(
|
|||
)
|
||||
|
||||
/**
|
||||
* Types of balances in P2P staking
|
||||
* Types of balances in P2PEthPool staking
|
||||
*/
|
||||
enum class P2PEthPoolBalanceType {
|
||||
STAKED,
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ enum class P2PEthPoolNetwork(
|
|||
}
|
||||
|
||||
/**
|
||||
* Check if chain ID is supported for P2P staking
|
||||
* Check if chain ID is supported for P2PEthPool staking
|
||||
*
|
||||
* @param chainId Ethereum chain ID
|
||||
* @return true if supported, false otherwise
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.tangem.domain.models.serialization.SerializedBigDecimal
|
|||
import org.joda.time.DateTime
|
||||
|
||||
/**
|
||||
* P2P.org rewards history entry
|
||||
* Rewards history entry
|
||||
* Historical reward information for account
|
||||
*/
|
||||
data class P2PEthPoolReward(
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ data class P2PEthPoolStaking(
|
|||
}
|
||||
|
||||
/**
|
||||
* Detailed vault information for P2P staking
|
||||
* Detailed vault information for P2PEthPool staking
|
||||
*/
|
||||
data class P2PEthPoolVaultDetails(
|
||||
val vaultAddress: String,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
package com.tangem.domain.staking.model.ethpool
|
||||
|
||||
/**
|
||||
* Configuration for P2P Ethereum staking network.
|
||||
* Configuration for P2PEthPool Ethereum staking network.
|
||||
*
|
||||
* Change [USE_TESTNET] to switch between testnet and mainnet.
|
||||
*/
|
||||
object P2PStakingConfig {
|
||||
object P2PEthPoolStakingConfig {
|
||||
|
||||
const val USE_TESTNET: Boolean = true
|
||||
|
||||
|
|
@ -3,7 +3,7 @@ package com.tangem.domain.staking.model.ethpool
|
|||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
|
||||
/**
|
||||
* P2P.org unsigned transaction ready for signing
|
||||
* Unsigned transaction ready for signing
|
||||
* Contains all necessary data for transaction signing
|
||||
*/
|
||||
data class P2PEthPoolUnsignedTx(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package com.tangem.domain.staking.model.stakekit
|
|||
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import com.tangem.domain.models.staking.YieldToken
|
||||
import com.tangem.domain.staking.model.common.RewardInfo
|
||||
import com.tangem.domain.staking.model.common.RewardType
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
|
|
@ -138,18 +140,6 @@ data class Yield(
|
|||
UNKNOWN,
|
||||
}
|
||||
}
|
||||
|
||||
enum class RewardType {
|
||||
APY, // compound rate
|
||||
APR, // simple rate
|
||||
UNKNOWN,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class RewardInfo(
|
||||
val rate: SerializedBigDecimal,
|
||||
val type: RewardType,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ import kotlinx.coroutines.launch
|
|||
|
||||
/**
|
||||
* Use case for fetching all staking options from all providers
|
||||
* Fetches both StakeKit yields and P2P vaults
|
||||
* Fetches both StakeKit yields and P2PEthPool vaults
|
||||
*/
|
||||
class FetchStakingOptionsUseCase(
|
||||
private val stakeKitRepository: StakeKitRepository,
|
||||
private val p2pRepository: P2PEthPoolRepository,
|
||||
private val p2pEthPoolRepository: P2PEthPoolRepository,
|
||||
private val stakingErrorResolver: StakingErrorResolver,
|
||||
) {
|
||||
suspend operator fun invoke(): Either<StakingError, Unit> {
|
||||
|
|
@ -25,7 +25,7 @@ class FetchStakingOptionsUseCase(
|
|||
block = {
|
||||
coroutineScope {
|
||||
launch { stakeKitRepository.fetchYields() }
|
||||
launch { p2pRepository.fetchVaults() }
|
||||
launch { p2pEthPoolRepository.fetchVaults() }
|
||||
}
|
||||
},
|
||||
catch = { stakingErrorResolver.resolve(it) },
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class GetStakingEntryInfoUseCase(
|
|||
symbol = symbol,
|
||||
)
|
||||
}
|
||||
is StakingOption.P2P -> {
|
||||
is StakingOption.P2PEthPool -> {
|
||||
StakingEntryInfo(
|
||||
tokenSymbol = "ETH",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
package com.tangem.domain.staking.model
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.staking.YieldToken
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* StakingIntegration implementation for P2PEthPool pooled staking.
|
||||
* Converts P2PEthPoolVault data to the common StakingIntegration interface.
|
||||
*/
|
||||
class P2PEthPoolIntegration(
|
||||
override val integrationId: StakingIntegrationID,
|
||||
vaults: List<P2PEthPoolVault>,
|
||||
) : StakingIntegration {
|
||||
|
||||
// Basic
|
||||
|
||||
override val token: YieldToken = YieldToken.ETH
|
||||
|
||||
override val tokens: List<YieldToken> = listOf(token)
|
||||
|
||||
// Targets (vaults)
|
||||
|
||||
override val targets: List<StakingTarget> = vaults.map { vault ->
|
||||
vault.toStakingTarget()
|
||||
}
|
||||
|
||||
override val preferredTargets: List<StakingTarget> = targets
|
||||
|
||||
override val areAllTargetsFull: Boolean = false
|
||||
|
||||
// Enter/Exit Args
|
||||
|
||||
override val isPartialAmountDisabled: Boolean = false
|
||||
|
||||
override val enterMinimumAmount: BigDecimal = DEFAULT_MINIMUM_STAKE
|
||||
|
||||
override val exitMinimumAmount: BigDecimal? = null
|
||||
|
||||
override val enterArgs: Yield.Args.Enter? = null
|
||||
|
||||
override val exitArgs: Yield.Args.Enter? = null
|
||||
|
||||
// Metadata
|
||||
|
||||
override val warmupPeriodDays: Int = 0
|
||||
|
||||
override val cooldownPeriodDays: Int = DEFAULT_COOLDOWN_DAYS
|
||||
|
||||
override val rewardSchedule: Yield.Metadata.RewardSchedule = Yield.Metadata.RewardSchedule.DAY
|
||||
|
||||
override val rewardClaiming: Yield.Metadata.RewardClaiming = Yield.Metadata.RewardClaiming.AUTO
|
||||
|
||||
override fun getCurrentToken(rawCurrencyId: CryptoCurrency.RawID?): YieldToken = token
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_COOLDOWN_DAYS = 7
|
||||
private val DEFAULT_MINIMUM_STAKE = BigDecimal("0.01")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.tangem.domain.staking.model
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.staking.YieldToken
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* StakingIntegration implementation for StakeKit.
|
||||
* Delegates all calls to the underlying Yield object.
|
||||
*/
|
||||
class StakeKitIntegration(
|
||||
override val integrationId: StakingIntegrationID,
|
||||
private val yield: Yield,
|
||||
) : StakingIntegration {
|
||||
|
||||
// Basic
|
||||
|
||||
override val token: YieldToken = yield.token
|
||||
|
||||
override val tokens: List<YieldToken> = yield.tokens
|
||||
|
||||
// Targets (validators)
|
||||
|
||||
override val targets: List<StakingTarget> = yield.validators.map { it.toStakingTarget() }
|
||||
|
||||
override val preferredTargets: List<StakingTarget> = yield.preferredValidators.map { it.toStakingTarget() }
|
||||
|
||||
override val areAllTargetsFull: Boolean = yield.allValidatorsFull
|
||||
|
||||
// Enter/Exit Args
|
||||
|
||||
override val isPartialAmountDisabled: Boolean = yield.args.enter.isPartialAmountDisabled
|
||||
|
||||
override val enterMinimumAmount: BigDecimal? =
|
||||
yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum
|
||||
|
||||
override val exitMinimumAmount: BigDecimal? =
|
||||
yield.args.exit?.args
|
||||
?.get(Yield.Args.ArgType.AMOUNT)?.minimum
|
||||
|
||||
override val enterArgs: Yield.Args.Enter = yield.args.enter
|
||||
|
||||
override val exitArgs: Yield.Args.Enter? = yield.args.exit
|
||||
|
||||
// Metadata
|
||||
|
||||
override val warmupPeriodDays: Int = yield.metadata.warmupPeriod.days
|
||||
|
||||
override val cooldownPeriodDays: Int? = yield.metadata.cooldownPeriod?.days
|
||||
|
||||
override val rewardSchedule: Yield.Metadata.RewardSchedule = yield.metadata.rewardSchedule
|
||||
|
||||
override val rewardClaiming: Yield.Metadata.RewardClaiming = yield.metadata.rewardClaiming
|
||||
|
||||
// Basic
|
||||
|
||||
override fun getCurrentToken(rawCurrencyId: CryptoCurrency.RawID?): YieldToken =
|
||||
tokens.firstOrNull { rawCurrencyId?.value == it.coinGeckoId } ?: token
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package com.tangem.domain.staking.model
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.staking.YieldToken
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Strategy interface for staking integrations.
|
||||
* Abstracts over StakeKit and P2PEthPool staking providers.
|
||||
*/
|
||||
// TODO p2p get rid of stakekit-specific models in StakingIntegration and implementors
|
||||
interface StakingIntegration {
|
||||
|
||||
// Basic
|
||||
|
||||
val integrationId: StakingIntegrationID
|
||||
|
||||
val token: YieldToken
|
||||
|
||||
val tokens: List<YieldToken>
|
||||
|
||||
// Targets (validators or vaults)
|
||||
|
||||
val targets: List<StakingTarget>
|
||||
|
||||
val preferredTargets: List<StakingTarget>
|
||||
|
||||
val areAllTargetsFull: Boolean
|
||||
|
||||
// Enter/Exit Args
|
||||
|
||||
val isPartialAmountDisabled: Boolean
|
||||
|
||||
val enterMinimumAmount: BigDecimal?
|
||||
|
||||
val exitMinimumAmount: BigDecimal?
|
||||
|
||||
val enterArgs: Yield.Args.Enter?
|
||||
|
||||
val exitArgs: Yield.Args.Enter?
|
||||
|
||||
// Metadata
|
||||
|
||||
val warmupPeriodDays: Int
|
||||
|
||||
val cooldownPeriodDays: Int?
|
||||
|
||||
val rewardSchedule: Yield.Metadata.RewardSchedule
|
||||
|
||||
val rewardClaiming: Yield.Metadata.RewardClaiming
|
||||
|
||||
// Basic
|
||||
|
||||
fun getCurrentToken(rawCurrencyId: CryptoCurrency.RawID?): YieldToken
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import com.tangem.blockchainsdk.utils.toBlockchain
|
|||
import com.tangem.blockchainsdk.utils.toMigratedCoinId
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
|
||||
|
||||
/**
|
||||
* Represents a staking integration identifier.
|
||||
|
|
@ -97,16 +97,14 @@ sealed interface StakingIntegrationID {
|
|||
}
|
||||
|
||||
/**
|
||||
* Represents P2P staking integrations
|
||||
* Represents P2PEthPool staking integration
|
||||
*/
|
||||
enum class P2P : StakingIntegrationID {
|
||||
EthereumPooled {
|
||||
override val value: String = "p2p-ethereum-pooled"
|
||||
override val blockchain: Blockchain
|
||||
get() = if (P2PStakingConfig.USE_TESTNET) Blockchain.EthereumTestnet else Blockchain.Ethereum
|
||||
override val networkId: String
|
||||
get() = P2PStakingConfig.activeNetwork.stakingNetworkId
|
||||
},
|
||||
object P2PEthPool : StakingIntegrationID {
|
||||
override val value: String = "p2p-ethereum-pooled"
|
||||
override val blockchain: Blockchain
|
||||
get() = if (P2PEthPoolStakingConfig.USE_TESTNET) Blockchain.EthereumTestnet else Blockchain.Ethereum
|
||||
override val networkId: String
|
||||
get() = P2PEthPoolStakingConfig.activeNetwork.stakingNetworkId
|
||||
}
|
||||
|
||||
// Polkadot {
|
||||
|
|
@ -138,7 +136,7 @@ sealed interface StakingIntegrationID {
|
|||
|
||||
/** List of all native staking integration IDs */
|
||||
val entries: List<StakingIntegrationID> by lazy {
|
||||
StakeKit.Coin.entries + StakeKit.EthereumToken.entries + P2P.entries
|
||||
StakeKit.Coin.entries + StakeKit.EthereumToken.entries + listOf(P2PEthPool)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -152,9 +150,12 @@ sealed interface StakingIntegrationID {
|
|||
val blockchain = Blockchain.fromId(id = currencyId.rawNetworkId)
|
||||
|
||||
return if (currencyId.contractAddress.isNullOrBlank()) {
|
||||
// Order is not important — either P2P or Stakekit.Coin can be in any order
|
||||
P2P.entries.firstOrNull { it.blockchain == blockchain }
|
||||
?: StakeKit.Coin.entries.firstOrNull { it.blockchain == blockchain }
|
||||
// Order is not important — either P2PEthPool or Stakekit.Coin can be in any order
|
||||
if (P2PEthPool.blockchain == blockchain) {
|
||||
P2PEthPool
|
||||
} else {
|
||||
StakeKit.Coin.entries.firstOrNull { it.blockchain == blockchain }
|
||||
}
|
||||
} else {
|
||||
StakeKit.EthereumToken.entries.firstOrNull { token ->
|
||||
token.blockchain == blockchain &&
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
package com.tangem.domain.staking.model
|
||||
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import com.tangem.domain.models.staking.NetworkType
|
||||
import com.tangem.domain.models.staking.YieldToken
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
|
||||
/**
|
||||
* Represents a staking option from any provider
|
||||
* Unified abstraction over StakeKit and P2P staking integrations
|
||||
* Unified abstraction over StakeKit and P2PEthPool staking integrations
|
||||
*/
|
||||
sealed interface StakingOption {
|
||||
|
||||
/** Unique identifier for the staking option */
|
||||
val integrationId: String
|
||||
val integrationId: StakingIntegrationID
|
||||
|
||||
/** Annual Percentage Yield */
|
||||
val apy: SerializedBigDecimal
|
||||
|
|
@ -28,34 +27,23 @@ sealed interface StakingOption {
|
|||
* StakeKit staking option
|
||||
* Wraps StakeKit Yield with all validator and metadata information
|
||||
*/
|
||||
data class StakeKit(val yield: Yield) : StakingOption {
|
||||
override val integrationId: String = yield.id
|
||||
data class StakeKit(
|
||||
override val integrationId: StakingIntegrationID.StakeKit,
|
||||
val yield: Yield,
|
||||
) : StakingOption {
|
||||
override val apy: SerializedBigDecimal = yield.apy
|
||||
override val token: YieldToken = yield.token
|
||||
override val isAvailable: Boolean = yield.isAvailable
|
||||
}
|
||||
|
||||
/**
|
||||
* P2P pooled staking option
|
||||
* Wraps P2P ETH Pool vault information
|
||||
* P2PEthPool staking option
|
||||
* Wraps P2PEthPool vault information
|
||||
*/
|
||||
data class P2P(val vaults: List<P2PEthPoolVault>) : StakingOption {
|
||||
override val integrationId: String = "p2p-ethereum-pooled"
|
||||
data class P2PEthPool(val vaults: List<P2PEthPoolVault>) : StakingOption {
|
||||
override val integrationId: StakingIntegrationID = StakingIntegrationID.P2PEthPool
|
||||
override val apy: SerializedBigDecimal = vaults.maxOf { it.apy }
|
||||
override val token: YieldToken = createEthToken()
|
||||
override val token: YieldToken = YieldToken.ETH
|
||||
override val isAvailable: Boolean = vaults.isNotEmpty()
|
||||
|
||||
private fun createEthToken(): YieldToken { // TODO
|
||||
return YieldToken(
|
||||
name = "Ethereum",
|
||||
network = NetworkType.ETHEREUM,
|
||||
symbol = "ETH",
|
||||
decimals = 18,
|
||||
address = null, // Native token
|
||||
coinGeckoId = "ethereum",
|
||||
logoURI = null,
|
||||
isPoints = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import com.tangem.domain.staking.model.ethpool.P2PEthPoolNetwork
|
|||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolReward
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolUnsignedTx
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
|
@ -17,24 +17,24 @@ interface P2PEthPoolRepository {
|
|||
/**
|
||||
* Fetch and store available staking vaults
|
||||
*
|
||||
* @param network P2P network (MAINNET or TESTNET)
|
||||
* @param network P2PEthPool network (MAINNET or TESTNET)
|
||||
*/
|
||||
suspend fun fetchVaults(network: P2PEthPoolNetwork = P2PStakingConfig.activeNetwork)
|
||||
suspend fun fetchVaults(network: P2PEthPoolNetwork = P2PEthPoolStakingConfig.activeNetwork)
|
||||
|
||||
/**
|
||||
* Get list of available staking vaults
|
||||
*
|
||||
* @param network P2P network (MAINNET or TESTNET)
|
||||
* @param network P2PEthPool network (MAINNET or TESTNET)
|
||||
* @return Either error or list of vaults with APY, capacity, fees
|
||||
*/
|
||||
suspend fun getVaults(
|
||||
network: P2PEthPoolNetwork = P2PStakingConfig.activeNetwork,
|
||||
network: P2PEthPoolNetwork = P2PEthPoolStakingConfig.activeNetwork,
|
||||
): Either<StakingError, List<P2PEthPoolVault>>
|
||||
|
||||
/**
|
||||
* Create unsigned transaction for depositing ETH into a vault
|
||||
*
|
||||
* @param network P2P network (MAINNET or TESTNET)
|
||||
* @param network P2PEthPool network (MAINNET or TESTNET)
|
||||
* @param delegatorAddress User's wallet address
|
||||
* @param vaultAddress Vault contract address
|
||||
* @param amount Amount of ETH to deposit
|
||||
|
|
@ -53,7 +53,7 @@ interface P2PEthPoolRepository {
|
|||
* Unstaking adds funds to exit queue. After ~1-4 days, use [createWithdrawTransaction]
|
||||
* to withdraw the funds.
|
||||
*
|
||||
* @param network P2P network (MAINNET or TESTNET)
|
||||
* @param network P2PEthPool network (MAINNET or TESTNET)
|
||||
* @param stakerPublicKey Staker's public key (note: API doc may have Bitcoin terminology)
|
||||
* @param stakeTransactionHash Original stake transaction hash
|
||||
* @return Either error or unsigned transaction
|
||||
|
|
@ -69,7 +69,7 @@ interface P2PEthPoolRepository {
|
|||
*
|
||||
* Only works when funds are available (after exit queue wait period).
|
||||
*
|
||||
* @param network P2P network (MAINNET or TESTNET)
|
||||
* @param network P2PEthPool network (MAINNET or TESTNET)
|
||||
* @param stakerAddress User's wallet address
|
||||
* @return Either error or unsigned transaction with withdrawal tickets
|
||||
*/
|
||||
|
|
@ -81,7 +81,7 @@ interface P2PEthPoolRepository {
|
|||
/**
|
||||
* Broadcast signed transaction to blockchain
|
||||
*
|
||||
* @param network P2P network (MAINNET or TESTNET)
|
||||
* @param network P2PEthPool network (MAINNET or TESTNET)
|
||||
* @param signedTransaction Signed transaction in hex format (with 0x prefix)
|
||||
* @return Either error or broadcast result with transaction hash
|
||||
*/
|
||||
|
|
@ -95,7 +95,7 @@ interface P2PEthPoolRepository {
|
|||
*
|
||||
* Returns current stake, rewards, exit queue status, and available amounts
|
||||
*
|
||||
* @param network P2P network (MAINNET or TESTNET)
|
||||
* @param network P2PEthPool network (MAINNET or TESTNET)
|
||||
* @param delegatorAddress User's wallet address
|
||||
* @param vaultAddress Vault contract address
|
||||
* @return Either error or account info
|
||||
|
|
@ -109,7 +109,7 @@ interface P2PEthPoolRepository {
|
|||
/**
|
||||
* Get rewards history for account and vault
|
||||
*
|
||||
* @param network P2P network (MAINNET or TESTNET)
|
||||
* @param network P2PEthPool network (MAINNET or TESTNET)
|
||||
* @param delegatorAddress User's wallet address
|
||||
* @param vaultAddress Vault contract address
|
||||
* @param period Optional period filter in days (30, 60, or 90)
|
||||
|
|
@ -123,17 +123,27 @@ interface P2PEthPoolRepository {
|
|||
): Either<StakingError, List<P2PEthPoolReward>>
|
||||
|
||||
/**
|
||||
* Check P2P staking availability by finding public vault
|
||||
* Get flow of cached vaults.
|
||||
*
|
||||
* @return Flow of StakingAvailability - Available with StakingOption.P2P if public vault found,
|
||||
* This returns vaults from the local cache/store.
|
||||
* Call [fetchVaults] first to populate the cache from the network.
|
||||
*
|
||||
* @return Flow of cached vaults list
|
||||
*/
|
||||
fun getVaultsFlow(): Flow<List<P2PEthPoolVault>>
|
||||
|
||||
/**
|
||||
* Check P2PEthPool staking availability by finding public vault
|
||||
*
|
||||
* @return Flow of StakingAvailability - Available with StakingOption.P2PEthPool if public vault found,
|
||||
* TemporaryUnavailable if not found or vaults empty
|
||||
*/
|
||||
fun getStakingAvailability(): Flow<StakingAvailability>
|
||||
|
||||
/**
|
||||
* Check P2P staking availability synchronously
|
||||
* Check P2PEthPool staking availability synchronously
|
||||
*
|
||||
* @return StakingAvailability - Available with StakingOption.P2P if public vault found,
|
||||
* @return StakingAvailability - Available with StakingOption.P2PEthPool if public vault found,
|
||||
* TemporaryUnavailable if not found or vaults empty
|
||||
*/
|
||||
suspend fun getStakingAvailabilitySync(): StakingAvailability
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.tangem.domain.models.network.Network
|
|||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.domain.staking.model.StakingEntryInfo
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.domain.models.staking.NetworkType
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingAction
|
||||
|
|
@ -24,9 +25,17 @@ interface StakeKitRepository {
|
|||
|
||||
fun getEnabledYields(): Flow<List<Yield>>
|
||||
|
||||
fun getStakingAvailability(rawCurrencyId: CryptoCurrency.RawID, symbol: String): Flow<StakingAvailability>
|
||||
fun getStakingAvailability(
|
||||
integrationId: StakingIntegrationID.StakeKit,
|
||||
rawCurrencyId: CryptoCurrency.RawID,
|
||||
symbol: String,
|
||||
): Flow<StakingAvailability>
|
||||
|
||||
suspend fun getStakingAvailabilitySync(rawCurrencyId: CryptoCurrency.RawID, symbol: String): StakingAvailability
|
||||
suspend fun getStakingAvailabilitySync(
|
||||
integrationId: StakingIntegrationID.StakeKit,
|
||||
rawCurrencyId: CryptoCurrency.RawID,
|
||||
symbol: String,
|
||||
): StakingAvailability
|
||||
|
||||
suspend fun getEntryInfo(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): StakingEntryInfo
|
||||
|
||||
|
|
|
|||
|
|
@ -2,35 +2,50 @@ package com.tangem.domain.staking.usecase
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toCoinId
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.StakingTarget
|
||||
import com.tangem.domain.staking.model.toStakingTarget
|
||||
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
|
||||
import com.tangem.domain.staking.repositories.StakeKitRepository
|
||||
import com.tangem.domain.staking.toggles.StakingFeatureToggles
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
||||
/**
|
||||
* Emits a map of Validators values per currency for staking.
|
||||
* Emits a map of StakingTarget values per currency for staking.
|
||||
*
|
||||
* Return map:
|
||||
* - key: currency staking key (network.backendId + "_" + symbol)
|
||||
* - value: validators
|
||||
* - key: currency staking key (coinGeckoId + "_" + symbol)
|
||||
* - value: list of staking targets (validators or vaults)
|
||||
*/
|
||||
class StakingApyFlowUseCase(
|
||||
private val stakeKitRepository: StakeKitRepository,
|
||||
private val p2pEthPoolRepository: P2PEthPoolRepository,
|
||||
private val stakingFeatureToggles: StakingFeatureToggles,
|
||||
) {
|
||||
|
||||
operator fun invoke(): Flow<Map<String, List<Yield.Validator>>> {
|
||||
return stakeKitRepository.getEnabledYields()
|
||||
.map { yields ->
|
||||
yields.filterNot { yield ->
|
||||
val isCardanoYield = yield.token.coinGeckoId == Blockchain.Cardano.toCoinId()
|
||||
isCardanoYield && !stakingFeatureToggles.isCardanoStakingEnabled
|
||||
}.associate { yield ->
|
||||
val key = "${yield.token.coinGeckoId}_${yield.token.symbol}"
|
||||
val apy = yield.validators
|
||||
key to apy
|
||||
}
|
||||
operator fun invoke(): Flow<Map<String, List<StakingTarget>>> {
|
||||
return combine(
|
||||
stakeKitRepository.getEnabledYields(),
|
||||
p2pEthPoolRepository.getVaultsFlow(),
|
||||
) { yields, p2pVaults ->
|
||||
val stakeKitMap = yields.filterNot { yield ->
|
||||
val isCardanoYield = yield.token.coinGeckoId == Blockchain.Cardano.toCoinId()
|
||||
isCardanoYield && !stakingFeatureToggles.isCardanoStakingEnabled
|
||||
}.associate { yield ->
|
||||
val key = "${yield.token.coinGeckoId}_${yield.token.symbol}"
|
||||
val targets = yield.validators.map { it.toStakingTarget() }
|
||||
key to targets
|
||||
}
|
||||
|
||||
val p2pMap = if (p2pVaults.isNotEmpty()) {
|
||||
val ethKey = "${Blockchain.Ethereum.toCoinId()}_${Blockchain.Ethereum.currency}"
|
||||
val targets = p2pVaults.map { it.toStakingTarget() }
|
||||
mapOf(ethKey to targets)
|
||||
} else {
|
||||
emptyMap()
|
||||
}
|
||||
|
||||
stakeKitMap + p2pMap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import java.math.BigDecimal
|
|||
|
||||
/**
|
||||
* Provider-agnostic extension to get total balance including rewards.
|
||||
* Works for both StakeKit and P2P providers.
|
||||
* Works for both StakeKit and P2PEthPool providers.
|
||||
*
|
||||
* Returns sum of all staking-related balances including rewards
|
||||
* (staked + unstaking + withdrawable + rewards).
|
||||
|
|
@ -18,7 +18,7 @@ import java.math.BigDecimal
|
|||
fun StakingBalance.Data.getTotalWithRewardsStakingBalance(blockchainId: String): BigDecimal {
|
||||
return when (this) {
|
||||
is StakingBalance.Data.StakeKit -> getTotalWithRewardsStakingBalanceStakeKit(blockchainId)
|
||||
is StakingBalance.Data.P2P -> {
|
||||
is StakingBalance.Data.P2PEthPool -> {
|
||||
val rewards = totalRewards
|
||||
if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId)) {
|
||||
totalStaked + unstakingAmount + withdrawableAmount + rewards
|
||||
|
|
@ -31,7 +31,7 @@ fun StakingBalance.Data.getTotalWithRewardsStakingBalance(blockchainId: String):
|
|||
|
||||
/**
|
||||
* Provider-agnostic extension to get total staking balance excluding rewards.
|
||||
* Works for both StakeKit and P2P providers.
|
||||
* Works for both StakeKit and P2PEthPool providers.
|
||||
*
|
||||
* Returns sum of all staking-related balances (staked + unstaking + withdrawable)
|
||||
* excluding rewards.
|
||||
|
|
@ -39,7 +39,7 @@ fun StakingBalance.Data.getTotalWithRewardsStakingBalance(blockchainId: String):
|
|||
fun StakingBalance.Data.getTotalStakingBalance(blockchainId: String): BigDecimal {
|
||||
return when (this) {
|
||||
is StakingBalance.Data.StakeKit -> getTotalStakingBalanceStakeKit(blockchainId)
|
||||
is StakingBalance.Data.P2P -> totalStaked + unstakingAmount + withdrawableAmount
|
||||
is StakingBalance.Data.P2PEthPool -> totalStaked + unstakingAmount + withdrawableAmount
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -149,8 +149,8 @@ internal class StakingIdFactoryTest {
|
|||
expected = createStakingId(integrationId = StakingIntegrationID.StakeKit.Coin.Cardano),
|
||||
),
|
||||
CreateModel(
|
||||
currencyId = createCurrencyId(blockchain = StakingIntegrationID.P2P.EthereumPooled.blockchain),
|
||||
expected = createStakingId(integrationId = StakingIntegrationID.P2P.EthereumPooled),
|
||||
currencyId = createCurrencyId(blockchain = StakingIntegrationID.P2PEthPool.blockchain),
|
||||
expected = createStakingId(integrationId = StakingIntegrationID.P2PEthPool),
|
||||
),
|
||||
CreateModel(
|
||||
currencyId = CryptoCurrency.ID.fromValue(
|
||||
|
|
|
|||
|
|
@ -38,17 +38,6 @@ class StakingIntegrationIDTest {
|
|||
Truth.assertThat(actual).hasSize(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all P2P blockchains are unique`() {
|
||||
// Act
|
||||
val actual = StakingIntegrationID.P2P.entries
|
||||
.distinctBy(StakingIntegrationID.P2P::blockchain)
|
||||
|
||||
// Assert
|
||||
val expected = StakingIntegrationID.P2P.entries.size
|
||||
Truth.assertThat(actual).hasSize(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all sub blockchains are unique`() {
|
||||
// Act
|
||||
|
|
@ -151,8 +140,8 @@ class StakingIntegrationIDTest {
|
|||
expected = StakingIntegrationID.StakeKit.Coin.Cardano,
|
||||
),
|
||||
CreateModel(
|
||||
currencyId = createCurrencyId(blockchain = StakingIntegrationID.P2P.EthereumPooled.blockchain),
|
||||
expected = StakingIntegrationID.P2P.EthereumPooled,
|
||||
currencyId = createCurrencyId(blockchain = StakingIntegrationID.P2PEthPool.blockchain),
|
||||
expected = StakingIntegrationID.P2PEthPool,
|
||||
),
|
||||
CreateModel(
|
||||
currencyId = CryptoCurrency.ID.fromValue(value = "token⟨ETH⟩polygon-ecosystem-token⚓1234567890"),
|
||||
|
|
|
|||
|
|
@ -267,7 +267,7 @@ object CryptoCurrencyStatusFactory {
|
|||
null
|
||||
}
|
||||
}
|
||||
is StakingBalance.Data.P2P -> {
|
||||
is StakingBalance.Data.P2PEthPool -> {
|
||||
// TODO p2p
|
||||
val isCurrentAddressStaking = stakingBalance.stakingId.address == address.defaultAddress.value
|
||||
if (isCurrentAddressStaking) stakingBalance else null
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ dependencies {
|
|||
implementation(projects.domain.markets)
|
||||
implementation(projects.domain.onramp.models)
|
||||
implementation(projects.domain.staking.models)
|
||||
implementation(projects.domain.staking)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets)
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
|
|||
AppRoute.Staking(
|
||||
userWalletId = cryptoCurrencyData.userWallet.walletId,
|
||||
cryptoCurrency = cryptoCurrencyData.status.currency,
|
||||
yieldId = option.integrationId,
|
||||
integrationId = option.integrationId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ dependencies {
|
|||
|
||||
/** Domain models */
|
||||
api(projects.domain.models)
|
||||
implementation(projects.domain.staking.models)
|
||||
implementation(projects.domain.staking)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.staking.api
|
||||
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
|
@ -10,7 +11,7 @@ interface StakingComponent : ComposableContentComponent {
|
|||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
val yieldId: String,
|
||||
val integrationId: StakingIntegrationID,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, StakingComponent>
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@ internal class StakingAnalyticSender(
|
|||
|
||||
fun initialInfoScreen(value: StakingUiState) {
|
||||
val initialInfoState = value.initialInfoState as? StakingStates.InitialInfoState.Data
|
||||
val validatorState = initialInfoState?.yieldBalance as? InnerYieldBalanceState.Data
|
||||
val validatorCount = validatorState?.balances
|
||||
?.filterNot { it.validator?.address.isNullOrBlank() }
|
||||
?.distinctBy { it.validator?.address }
|
||||
val balanceState = initialInfoState?.yieldBalance as? InnerYieldBalanceState.Data
|
||||
val validatorCount = balanceState?.balances
|
||||
?.filterNot { it.target?.address.isNullOrBlank() }
|
||||
?.distinctBy { it.target?.address }
|
||||
?.size ?: 0
|
||||
|
||||
analyticsEventHandler.send(
|
||||
|
|
@ -34,7 +34,7 @@ internal class StakingAnalyticSender(
|
|||
fun confirmationScreen(value: StakingUiState) {
|
||||
val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
val validatorState = value.validatorState as? StakingStates.ValidatorState.Data
|
||||
val validatorName = validatorState?.chosenValidator?.name ?: return
|
||||
val validatorName = validatorState?.chosenTarget?.name ?: return
|
||||
|
||||
if (confirmationState?.innerState == InnerConfirmationStakingState.COMPLETED) return
|
||||
|
||||
|
|
@ -80,7 +80,7 @@ internal class StakingAnalyticSender(
|
|||
|
||||
fun sendTransactionStakingAnalytics(value: StakingUiState, cryptoCurrencyStatus: CryptoCurrencyStatus) {
|
||||
val validatorState = value.validatorState as? StakingStates.ValidatorState.Data
|
||||
val validatorName = validatorState?.chosenValidator?.name ?: return
|
||||
val validatorName = validatorState?.chosenTarget?.name ?: return
|
||||
|
||||
analyticsEventHandler.send(
|
||||
Basic.TransactionSent(
|
||||
|
|
@ -102,7 +102,7 @@ internal class StakingAnalyticSender(
|
|||
|
||||
fun sendTransactionStakingClickedAnalytics(value: StakingUiState) {
|
||||
val validatorState = value.validatorState as? StakingStates.ValidatorState.Data
|
||||
val validatorName = validatorState?.chosenValidator?.name ?: return
|
||||
val validatorName = validatorState?.chosenTarget?.name ?: return
|
||||
|
||||
analyticsEventHandler.send(
|
||||
StakingAnalyticsEvent.ButtonAction(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.features.staking.impl.deeplink
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY
|
||||
|
|
@ -8,7 +7,6 @@ import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY
|
|||
import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
|
||||
import com.tangem.domain.staking.GetYieldUseCase
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
|
||||
|
|
@ -28,7 +26,6 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
|
|||
private val appRouter: AppRouter,
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
private val getYieldUseCase: GetYieldUseCase,
|
||||
private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase,
|
||||
) : StakingDeepLinkHandler {
|
||||
|
||||
|
|
@ -73,19 +70,13 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
|
|||
return@launch
|
||||
}
|
||||
|
||||
val isStakingEnabled = getStakingAvailabilityUseCase.invokeSync(
|
||||
val availability = getStakingAvailabilityUseCase.invokeSync(
|
||||
userWalletId = selectedUserWalletId,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
).getOrNull()
|
||||
|
||||
if (isStakingEnabled !is StakingAvailability.Available) {
|
||||
return@launch
|
||||
}
|
||||
|
||||
val yield = getYieldUseCase.invoke(
|
||||
cryptoCurrencyId = cryptoCurrency.id,
|
||||
symbol = cryptoCurrency.symbol,
|
||||
).getOrElse {
|
||||
val option = (availability as? StakingAvailability.Available)?.option
|
||||
if (option == null) {
|
||||
Timber.e("Staking is unavailable for ${cryptoCurrency.name}")
|
||||
return@launch
|
||||
}
|
||||
|
|
@ -94,7 +85,7 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
|
|||
AppRoute.Staking(
|
||||
userWalletId = selectedUserWalletId,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
yieldId = yield.id,
|
||||
integrationId = option.integrationId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
|
|||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.StakingTarget
|
||||
import com.tangem.features.staking.impl.presentation.state.BalanceState
|
||||
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -35,7 +35,7 @@ internal interface StakingClickIntents : AmountScreenClickIntents {
|
|||
|
||||
fun openValidators()
|
||||
|
||||
fun onValidatorSelect(validator: Yield.Validator)
|
||||
fun onTargetSelect(target: StakingTarget)
|
||||
|
||||
fun openRewardsValidators()
|
||||
|
||||
|
|
|
|||
|
|
@ -45,11 +45,15 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
import com.tangem.domain.staking.*
|
||||
import com.tangem.domain.staking.analytics.StakeScreenSource
|
||||
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent
|
||||
import com.tangem.domain.staking.model.P2PEthPoolIntegration
|
||||
import com.tangem.domain.staking.model.StakeKitIntegration
|
||||
import com.tangem.domain.staking.model.StakingApproval
|
||||
import com.tangem.domain.staking.model.StakingIntegration
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.domain.staking.model.StakingTarget
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingAction
|
||||
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
|
||||
import com.tangem.domain.staking.utils.getValidatorsCount
|
||||
|
|
@ -134,6 +138,7 @@ internal class StakingModel @Inject constructor(
|
|||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val getActionsUseCase: GetActionsUseCase,
|
||||
private val getYieldUseCase: GetYieldUseCase,
|
||||
private val p2pEthPoolRepository: P2PEthPoolRepository,
|
||||
private val checkAccountInitializedUseCase: CheckAccountInitializedUseCase,
|
||||
private val createTransferTransactionUseCase: CreateTransferTransactionUseCase,
|
||||
private val getFeeUseCase: GetFeeUseCase,
|
||||
|
|
@ -164,9 +169,18 @@ internal class StakingModel @Inject constructor(
|
|||
|
||||
private val cryptoCurrencyId: CryptoCurrency.ID = params.cryptoCurrency.id
|
||||
private val userWalletId: UserWalletId = params.userWalletId
|
||||
private val yield: Yield = runBlocking {
|
||||
getYieldUseCase(params.yieldId).getOrElse {
|
||||
error("yield must be not null")
|
||||
private val integration: StakingIntegration = runBlocking {
|
||||
when (val integrationId = params.integrationId) {
|
||||
is StakingIntegrationID.StakeKit -> {
|
||||
val yield = getYieldUseCase(integrationId.value).getOrElse {
|
||||
error("yield must be not null")
|
||||
}
|
||||
StakeKitIntegration(integrationId, yield)
|
||||
}
|
||||
StakingIntegrationID.P2PEthPool -> {
|
||||
val vaults = p2pEthPoolRepository.getVaults().getOrElse { emptyList() }
|
||||
P2PEthPoolIntegration(integrationId, vaults)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -194,7 +208,7 @@ internal class StakingModel @Inject constructor(
|
|||
return invalidatePendingTransactionsUseCase(
|
||||
balanceItems = stakeKitBalance?.balance?.items.orEmpty(),
|
||||
stakingActions = stakingActions,
|
||||
token = yield.token,
|
||||
token = integration.token,
|
||||
).getOrElse { emptyList() }
|
||||
}
|
||||
|
||||
|
|
@ -205,7 +219,7 @@ internal class StakingModel @Inject constructor(
|
|||
stakingBalanceUpdater.create(
|
||||
cryptoCurrencyStatus,
|
||||
userWallet,
|
||||
yield,
|
||||
integration,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -213,7 +227,7 @@ internal class StakingModel @Inject constructor(
|
|||
stakingFeeTransactionLoader.create(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
userWallet = userWallet,
|
||||
yield = yield,
|
||||
integration = integration,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -221,7 +235,7 @@ internal class StakingModel @Inject constructor(
|
|||
stakingTransactionLoader.create(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
userWallet = userWallet,
|
||||
yield = yield,
|
||||
integration = integration,
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||
)
|
||||
}
|
||||
|
|
@ -280,7 +294,7 @@ internal class StakingModel @Inject constructor(
|
|||
val hasNoYieldBalanceData = cryptoCurrencyStatus.value.stakingBalance !is StakingBalance.Data.StakeKit
|
||||
|
||||
when {
|
||||
isInitialInfoStep && noBalanceState && yield.allValidatorsFull && hasNoYieldBalanceData -> {
|
||||
isInitialInfoStep && noBalanceState && integration.areAllTargetsFull && hasNoYieldBalanceData -> {
|
||||
stakingEventFactory.createStakingValidatorsUnavailableAlert()
|
||||
return@launch
|
||||
}
|
||||
|
|
@ -293,12 +307,12 @@ internal class StakingModel @Inject constructor(
|
|||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
stakingApproval = stakingApproval,
|
||||
stakingAllowance = stakingAllowance,
|
||||
yieldArgs = yield.args,
|
||||
integration = integration,
|
||||
).let(::add)
|
||||
if (yield.args.enter.isPartialAmountDisabled) {
|
||||
if (integration.isPartialAmountDisabled) {
|
||||
ValidatorSelectChangeTransformer(
|
||||
selectedValidator = yield.preferredValidators.firstOrNull(),
|
||||
yield = yield,
|
||||
selectedTarget = integration.preferredTargets.firstOrNull(),
|
||||
integration = integration,
|
||||
).let(::add)
|
||||
SetAmountDataTransformer(
|
||||
clickIntents = this@StakingModel,
|
||||
|
|
@ -313,7 +327,7 @@ internal class StakingModel @Inject constructor(
|
|||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
minimumTransactionAmount = minimumTransactionAmount,
|
||||
actionType = uiState.value.actionType,
|
||||
yield = yield,
|
||||
integration = integration,
|
||||
).let(::add)
|
||||
}
|
||||
}
|
||||
|
|
@ -327,7 +341,7 @@ internal class StakingModel @Inject constructor(
|
|||
override fun getFee() {
|
||||
stateController.update(
|
||||
SetConfirmationStateLoadingTransformer(
|
||||
yield = yield,
|
||||
integration = integration,
|
||||
appCurrency = appCurrency,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
),
|
||||
|
|
@ -479,7 +493,7 @@ internal class StakingModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onAmountEnterClick() {
|
||||
if (yield.preferredValidators.isEmpty()) {
|
||||
if (integration.preferredTargets.isEmpty()) {
|
||||
stateController.updateEvent(
|
||||
StakingEvent.ShowAlert(StakingAlertUM.NoAvailableValidators),
|
||||
)
|
||||
|
|
@ -487,8 +501,8 @@ internal class StakingModel @Inject constructor(
|
|||
if (uiState.value.actionType is StakingActionCommonType.Enter) {
|
||||
stateController.updateAll(
|
||||
ValidatorSelectChangeTransformer(
|
||||
selectedValidator = null,
|
||||
yield = yield,
|
||||
selectedTarget = null,
|
||||
integration = integration,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -502,7 +516,7 @@ internal class StakingModel @Inject constructor(
|
|||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
minimumTransactionAmount = minimumTransactionAmount,
|
||||
value = value,
|
||||
yield = yield,
|
||||
integration = integration,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -518,7 +532,7 @@ internal class StakingModel @Inject constructor(
|
|||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
minimumTransactionAmount = minimumTransactionAmount,
|
||||
actionType = uiState.value.actionType,
|
||||
yield = yield,
|
||||
integration = integration,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -539,16 +553,16 @@ internal class StakingModel @Inject constructor(
|
|||
stakingStateRouter.showValidators()
|
||||
}
|
||||
|
||||
override fun onValidatorSelect(validator: Yield.Validator) {
|
||||
override fun onTargetSelect(target: StakingTarget) {
|
||||
analyticsEventHandler.send(
|
||||
StakingAnalyticsEvent.ValidatorChosen(
|
||||
validator = validator.name,
|
||||
validator = target.name,
|
||||
),
|
||||
)
|
||||
stateController.update(
|
||||
ValidatorSelectChangeTransformer(
|
||||
selectedValidator = validator,
|
||||
yield = yield,
|
||||
selectedTarget = target,
|
||||
integration = integration,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -606,7 +620,7 @@ internal class StakingModel @Inject constructor(
|
|||
balanceType = activeStake.type,
|
||||
pendingActions = activeStake.pendingActions,
|
||||
balanceState = activeStake,
|
||||
validator = activeStake.validator,
|
||||
target = activeStake.target,
|
||||
amountValue = activeStake.cryptoValue,
|
||||
)
|
||||
onNextClick(activeStake)
|
||||
|
|
@ -619,7 +633,7 @@ internal class StakingModel @Inject constructor(
|
|||
balanceType = activeStake.type,
|
||||
pendingAction = action,
|
||||
balanceState = activeStake,
|
||||
validator = activeStake.validator,
|
||||
target = activeStake.target,
|
||||
amountValue = activeStake.cryptoValue,
|
||||
)
|
||||
stateController.update(DismissBottomSheetStateTransformer)
|
||||
|
|
@ -788,7 +802,7 @@ internal class StakingModel @Inject constructor(
|
|||
isSubtractAvailable = isAmountSubtractAvailable,
|
||||
feeError = feeError,
|
||||
stakingError = stakingError,
|
||||
yield = yield,
|
||||
integration = integration,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -911,7 +925,7 @@ internal class StakingModel @Inject constructor(
|
|||
val validatorState = uiState.value.validatorState as? StakingStates.ValidatorState.Data
|
||||
val feeState = confirmationState?.feeState as? FeeState.Content
|
||||
|
||||
val validator = validatorState?.chosenValidator
|
||||
val target = validatorState?.chosenTarget
|
||||
val feeAmount = feeState?.fee?.amount
|
||||
val amount = amountState?.amountTextField?.cryptoAmount
|
||||
saveBlockchainErrorUseCase(
|
||||
|
|
@ -919,7 +933,7 @@ internal class StakingModel @Inject constructor(
|
|||
errorMessage = errorMessage,
|
||||
blockchainId = network.rawId,
|
||||
derivationPath = network.derivationPath.value,
|
||||
destinationAddress = validator?.address.orEmpty(),
|
||||
destinationAddress = target?.address.orEmpty(),
|
||||
tokenSymbol = (cryptoCurrencyStatus.currency as? CryptoCurrency.Token)?.symbol,
|
||||
amount = amount?.run { value?.toPlainString() + currencySymbol }.orEmpty(),
|
||||
fee = feeAmount?.run { value?.toPlainString() + currencySymbol }.orEmpty(),
|
||||
|
|
@ -928,7 +942,7 @@ internal class StakingModel @Inject constructor(
|
|||
|
||||
val email = FeedbackEmailType.StakingProblem(
|
||||
walletMetaInfo = metaInfo,
|
||||
validatorName = validator?.name,
|
||||
validatorName = target?.name,
|
||||
transactionTypes = transactionsInProgress.map { it.type.name },
|
||||
unsignedTransactions = transactionsInProgress.map { it.unsignedTransaction },
|
||||
)
|
||||
|
|
@ -1229,7 +1243,7 @@ internal class StakingModel @Inject constructor(
|
|||
stateController.updateAll(
|
||||
SetInitialDataStateTransformer(
|
||||
clickIntents = this@StakingModel,
|
||||
yield = yield,
|
||||
integration = integration,
|
||||
isAnyTokenStaked = isAnyTokenStaked,
|
||||
cryptoCurrencyStatus = status,
|
||||
userWalletProvider = Provider { userWallet },
|
||||
|
|
@ -1248,7 +1262,7 @@ internal class StakingModel @Inject constructor(
|
|||
balanceState: BalanceState,
|
||||
pendingActions: ImmutableList<PendingAction> = persistentListOf(),
|
||||
pendingAction: PendingAction? = pendingActions.firstOrNull(),
|
||||
validator: Yield.Validator?,
|
||||
target: StakingTarget?,
|
||||
amountValue: String,
|
||||
) {
|
||||
stateController.updateAll(
|
||||
|
|
@ -1261,11 +1275,11 @@ internal class StakingModel @Inject constructor(
|
|||
pendingActions = pendingActions,
|
||||
pendingAction = pendingAction,
|
||||
stakingAllowance = stakingAllowance,
|
||||
yieldArgs = yield.args,
|
||||
integration = integration,
|
||||
),
|
||||
ValidatorSelectChangeTransformer(
|
||||
selectedValidator = validator,
|
||||
yield = yield,
|
||||
selectedTarget = target,
|
||||
integration = integration,
|
||||
),
|
||||
SetAmountDataTransformer(
|
||||
clickIntents = this,
|
||||
|
|
@ -1280,7 +1294,7 @@ internal class StakingModel @Inject constructor(
|
|||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
value = amountValue,
|
||||
minimumTransactionAmount = minimumTransactionAmount,
|
||||
yield = yield,
|
||||
integration = integration,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import com.tangem.domain.models.staking.BalanceType
|
|||
import com.tangem.domain.models.staking.PendingAction
|
||||
import com.tangem.domain.models.staking.PendingActionConstraints
|
||||
import com.tangem.domain.models.staking.RewardBlockType
|
||||
import com.tangem.domain.staking.model.stakekit.*
|
||||
import com.tangem.domain.staking.model.StakingTarget
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
|
@ -35,10 +35,10 @@ internal data class BalanceState(
|
|||
val fiatAmount: BigDecimal?,
|
||||
val formattedFiatAmount: TextReference,
|
||||
val rawCurrencyId: String?,
|
||||
val validator: Yield.Validator?,
|
||||
val target: StakingTarget?,
|
||||
val pendingActions: ImmutableList<PendingAction>,
|
||||
val isPending: Boolean,
|
||||
val validatorAddress: String?,
|
||||
val targetAddress: String?,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import com.tangem.core.ui.event.StateEvent
|
|||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
|
||||
import com.tangem.domain.models.staking.PendingAction
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.StakingTarget
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
|
||||
import com.tangem.features.staking.impl.presentation.state.events.StakingEvent
|
||||
|
|
@ -97,9 +97,9 @@ internal sealed class StakingStates {
|
|||
override val isPrimaryButtonEnabled: Boolean,
|
||||
override val isClickable: Boolean,
|
||||
val isVisibleOnConfirmation: Boolean,
|
||||
val chosenValidator: Yield.Validator,
|
||||
val activeValidator: Yield.Validator?,
|
||||
val availableValidators: List<Yield.Validator>,
|
||||
val chosenTarget: StakingTarget,
|
||||
val activeTarget: StakingTarget?,
|
||||
val availableTargets: List<StakingTarget>,
|
||||
) : ValidatorState()
|
||||
|
||||
data class Empty(
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import com.tangem.domain.models.staking.BalanceType
|
|||
import com.tangem.domain.models.staking.BalanceType.Companion.isClickable
|
||||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.models.staking.action.StakingActionType
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.StakingIntegration
|
||||
import com.tangem.domain.staking.utils.getRewardStakingBalance
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.BalanceState
|
||||
|
|
@ -29,24 +29,24 @@ import java.util.Calendar
|
|||
internal class BalanceItemConverter(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val yield: Yield,
|
||||
private val integration: StakingIntegration,
|
||||
) : Converter<BalanceItem, BalanceState?> {
|
||||
|
||||
override fun convert(value: BalanceItem): BalanceState? {
|
||||
val appCurrency = appCurrencyProvider()
|
||||
val cryptoCurrency = cryptoCurrencyStatus.currency
|
||||
|
||||
val validator = yield.validators.firstOrNull {
|
||||
val target = integration.targets.firstOrNull {
|
||||
value.validatorAddress?.contains(it.address, ignoreCase = true) == true
|
||||
}
|
||||
val cryptoAmount = value.getBalanceValue()
|
||||
val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount)
|
||||
|
||||
val title = value.type.getTitle(validator?.name)
|
||||
val title = value.type.getTitle(target?.name)
|
||||
return title?.let {
|
||||
BalanceState(
|
||||
groupId = value.groupId,
|
||||
validator = validator,
|
||||
target = target,
|
||||
title = title,
|
||||
subtitle = getSubtitle(value),
|
||||
type = value.type,
|
||||
|
|
@ -68,7 +68,7 @@ internal class BalanceItemConverter(
|
|||
pendingActions = value.pendingActions.toPersistentList(),
|
||||
isClickable = value.isClickable(),
|
||||
isPending = value.isPending,
|
||||
validatorAddress = value.validatorAddress,
|
||||
targetAddress = value.validatorAddress,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -108,7 +108,7 @@ internal class BalanceItemConverter(
|
|||
resourceReference(R.string.staking_tap_to_unlock)
|
||||
}
|
||||
BalanceType.PREPARING -> {
|
||||
val warmupPeriod = yield.metadata.warmupPeriod.days
|
||||
val warmupPeriod = integration.warmupPeriodDays
|
||||
combinedReference(
|
||||
resourceReference(R.string.staking_details_warmup_period),
|
||||
stringReference(" "),
|
||||
|
|
@ -124,7 +124,7 @@ internal class BalanceItemConverter(
|
|||
}
|
||||
|
||||
private fun getUnbondingDate(date: Instant?): TextReference? {
|
||||
val unbondingPeriod = yield.metadata.cooldownPeriod?.days ?: return null
|
||||
val unbondingPeriod = integration.cooldownPeriodDays ?: return null
|
||||
if (date == null) {
|
||||
return combinedReference(
|
||||
resourceReference(R.string.staking_details_unbonding_period),
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
|||
import com.tangem.domain.models.staking.BalanceItem
|
||||
import com.tangem.domain.models.staking.BalanceType
|
||||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.StakingIntegration
|
||||
import com.tangem.domain.staking.model.StakingTarget
|
||||
import com.tangem.features.staking.impl.presentation.state.BalanceState
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.utils.Provider
|
||||
|
|
@ -21,7 +22,7 @@ import java.math.BigDecimal
|
|||
internal class RewardsValidatorStateConverter(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val yield: Yield,
|
||||
private val integration: StakingIntegration,
|
||||
) : Converter<Unit, StakingStates.RewardsValidatorsState> {
|
||||
override fun convert(value: Unit): StakingStates.RewardsValidatorsState {
|
||||
val stakingBalance = cryptoCurrencyStatus.value.stakingBalance
|
||||
|
|
@ -42,13 +43,13 @@ internal class RewardsValidatorStateConverter(
|
|||
|
||||
private fun List<BalanceItem>.mapRewardBalances(cryptoCurrencyStatus: CryptoCurrencyStatus) =
|
||||
this.mapNotNull { balance ->
|
||||
val validator = yield.validators.firstOrNull {
|
||||
val target = integration.targets.firstOrNull {
|
||||
it.address.contains(balance.validatorAddress.orEmpty(), ignoreCase = true)
|
||||
}
|
||||
val cryptoValue = balance.amount
|
||||
val fiatValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoValue)
|
||||
|
||||
validator?.toBalanceState(
|
||||
target?.toBalanceState(
|
||||
balance = balance,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
cryptoValue = cryptoValue,
|
||||
|
|
@ -56,7 +57,7 @@ internal class RewardsValidatorStateConverter(
|
|||
)
|
||||
}
|
||||
|
||||
private fun Yield.Validator.toBalanceState(
|
||||
private fun StakingTarget.toBalanceState(
|
||||
balance: BalanceItem,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
cryptoValue: BigDecimal,
|
||||
|
|
@ -80,7 +81,7 @@ internal class RewardsValidatorStateConverter(
|
|||
|
||||
return BalanceState(
|
||||
groupId = balance.groupId,
|
||||
validator = this,
|
||||
target = this,
|
||||
title = stringReference(this.name),
|
||||
subtitle = null,
|
||||
cryptoValue = cryptoValue.parseBigDecimal(cryptoCurrency.decimals),
|
||||
|
|
@ -93,7 +94,7 @@ internal class RewardsValidatorStateConverter(
|
|||
isClickable = true,
|
||||
type = balance.type,
|
||||
isPending = balance.isPending,
|
||||
validatorAddress = balance.validatorAddress,
|
||||
targetAddress = balance.validatorAddress,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ import com.tangem.domain.models.staking.BalanceType
|
|||
import com.tangem.domain.models.staking.RewardBlockType
|
||||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.models.staking.action.StakingActionType
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.StakingIntegration
|
||||
import com.tangem.domain.staking.utils.getRewardStakingBalance
|
||||
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
|
||||
import com.tangem.features.staking.impl.presentation.state.YieldReward
|
||||
|
|
@ -25,11 +25,11 @@ internal class YieldBalancesConverter(
|
|||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val balancesToShowProvider: Provider<List<BalanceItem>>,
|
||||
private val yield: Yield,
|
||||
private val integration: StakingIntegration,
|
||||
) : Converter<Unit, InnerYieldBalanceState> {
|
||||
|
||||
private val balanceItemConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
BalanceItemConverter(cryptoCurrencyStatus, appCurrencyProvider, yield)
|
||||
BalanceItemConverter(cryptoCurrencyStatus, appCurrencyProvider, integration)
|
||||
}
|
||||
|
||||
override fun convert(value: Unit): InnerYieldBalanceState {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
|||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.staking.FetchActionsUseCase
|
||||
import com.tangem.domain.staking.FetchStakingYieldBalanceUseCase
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.StakingIntegration
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus
|
||||
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.FetchPendingTransactionsUseCase
|
||||
|
|
@ -27,7 +27,7 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
|
|||
@DelayedWork private val coroutineScope: CoroutineScope,
|
||||
@Assisted private val userWallet: UserWallet,
|
||||
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
@Assisted private val yield: Yield,
|
||||
@Assisted private val integration: StakingIntegration,
|
||||
) {
|
||||
fun updateAfterTransaction() {
|
||||
coroutineScope.launch {
|
||||
|
|
@ -105,7 +105,7 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
|
|||
fetchActionsUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
networkType = yield.token.network,
|
||||
networkType = integration.token.network,
|
||||
stakingActionStatus = StakingActionStatus.PROCESSING,
|
||||
)
|
||||
}
|
||||
|
|
@ -115,7 +115,7 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
|
|||
fun create(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
userWallet: UserWallet,
|
||||
yield: Yield,
|
||||
integration: StakingIntegration,
|
||||
): StakingBalanceUpdater
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,12 +12,11 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
|||
import com.tangem.domain.models.staking.PendingAction
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.staking.EstimateGasUseCase
|
||||
import com.tangem.domain.staking.model.StakingIntegration
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
|
||||
import com.tangem.domain.tokens.model.staking.getCurrentToken
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
||||
|
|
@ -45,7 +44,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
|||
private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase,
|
||||
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
@Assisted private val userWallet: UserWallet,
|
||||
@Assisted private val yield: Yield,
|
||||
@Assisted private val integration: StakingIntegration,
|
||||
) {
|
||||
|
||||
suspend fun getFee(
|
||||
|
|
@ -58,9 +57,9 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
|||
val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
?: error("Illegal state")
|
||||
|
||||
val validatorAddress = (state.validatorState as? StakingStates.ValidatorState.Data)?.chosenValidator?.address
|
||||
?: state.balanceState?.validatorAddress
|
||||
?: error("No validator address provided")
|
||||
val validatorAddress = (state.validatorState as? StakingStates.ValidatorState.Data)?.chosenTarget?.address
|
||||
?: state.balanceState?.targetAddress
|
||||
?: error("No target address provided")
|
||||
|
||||
val amount = (state.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
|
||||
?: error("No amount provided")
|
||||
|
|
@ -170,11 +169,11 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
|||
network = cryptoCurrencyStatus.currency.network,
|
||||
params = ActionParams(
|
||||
actionCommonType = stateController.value.actionType,
|
||||
integrationId = yield.id,
|
||||
integrationId = integration.integrationId.value,
|
||||
amount = amount,
|
||||
address = sourceAddress,
|
||||
validatorAddress = validatorAddress,
|
||||
token = yield.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId),
|
||||
token = integration.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId),
|
||||
passthrough = action?.passthrough,
|
||||
type = action?.type,
|
||||
),
|
||||
|
|
@ -234,7 +233,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
|||
fun create(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
userWallet: UserWallet,
|
||||
yield: Yield,
|
||||
integration: StakingIntegration,
|
||||
): StakingFeeTransactionLoader
|
||||
}
|
||||
}
|
||||
|
|
@ -13,15 +13,14 @@ import com.tangem.domain.staking.GetConstructedStakingTransactionUseCase
|
|||
import com.tangem.domain.staking.GetStakingTransactionsUseCase
|
||||
import com.tangem.domain.staking.SaveUnsubmittedHashUseCase
|
||||
import com.tangem.domain.staking.SubmitHashUseCase
|
||||
import com.tangem.domain.staking.model.StakingIntegration
|
||||
import com.tangem.domain.staking.model.SubmitHashData
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionStatus
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType
|
||||
import com.tangem.domain.tokens.model.staking.getCurrentToken
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
|
|
@ -56,12 +55,12 @@ internal class StakingTransactionSender @AssistedInject constructor(
|
|||
private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
|
||||
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
@Assisted private val userWallet: UserWallet,
|
||||
@Assisted private val yield: Yield,
|
||||
@Assisted private val integration: StakingIntegration,
|
||||
@Assisted private val isAmountSubtractAvailable: Boolean,
|
||||
) {
|
||||
|
||||
private val balanceUpdater: StakingBalanceUpdater
|
||||
get() = stakingBalanceUpdater.create(cryptoCurrencyStatus, userWallet, yield)
|
||||
get() = stakingBalanceUpdater.create(cryptoCurrencyStatus, userWallet, integration)
|
||||
|
||||
suspend fun constructAndSendTransactions(
|
||||
onConstructSuccess: (List<StakingTransaction>) -> Unit,
|
||||
|
|
@ -193,7 +192,7 @@ internal class StakingTransactionSender @AssistedInject constructor(
|
|||
val amountState = state.amountState as? AmountState.Data
|
||||
?: error("No amount provided")
|
||||
|
||||
val validatorAddress = validatorState.chosenValidator.address
|
||||
val validatorAddress = validatorState.chosenTarget.address
|
||||
val amount = getAmount(amountState, fee, confirmationState.reduceAmountBy)
|
||||
|
||||
return getStakingTransactionsUseCase(
|
||||
|
|
@ -201,11 +200,11 @@ internal class StakingTransactionSender @AssistedInject constructor(
|
|||
network = cryptoCurrencyStatus.currency.network,
|
||||
params = ActionParams(
|
||||
actionCommonType = state.actionType,
|
||||
integrationId = yield.id,
|
||||
integrationId = integration.integrationId.value,
|
||||
amount = amount,
|
||||
address = defaultAddress,
|
||||
validatorAddress = validatorAddress,
|
||||
token = yield.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId),
|
||||
token = integration.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId),
|
||||
passthrough = action?.passthrough,
|
||||
type = action?.type,
|
||||
),
|
||||
|
|
@ -306,7 +305,7 @@ internal class StakingTransactionSender @AssistedInject constructor(
|
|||
fun create(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
userWallet: UserWallet,
|
||||
yield: Yield,
|
||||
integration: StakingIntegration,
|
||||
isAmountSubtractAvailable: Boolean,
|
||||
): StakingTransactionSender
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.tangem.core.ui.extensions.stringReference
|
|||
import com.tangem.domain.models.staking.BalanceType
|
||||
import com.tangem.domain.models.staking.RewardBlockType
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.toStakingTarget
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.BalanceState
|
||||
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
|
||||
|
|
@ -81,7 +82,7 @@ internal object InitialStakingStatePreview {
|
|||
fiatAmount = null,
|
||||
formattedFiatAmount = stringReference("100 $"),
|
||||
rawCurrencyId = null,
|
||||
validator = Yield.Validator(
|
||||
target = Yield.Validator(
|
||||
address = "address",
|
||||
status = Yield.Validator.ValidatorStatus.ACTIVE,
|
||||
name = "Binance",
|
||||
|
|
@ -92,13 +93,13 @@ internal object InitialStakingStatePreview {
|
|||
votingPower = null,
|
||||
preferred = false,
|
||||
isStrategicPartner = false,
|
||||
),
|
||||
).toStakingTarget(),
|
||||
pendingActions = persistentListOf(),
|
||||
isClickable = true,
|
||||
type = BalanceType.STAKED,
|
||||
subtitle = null,
|
||||
isPending = false,
|
||||
validatorAddress = "",
|
||||
targetAddress = "",
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.previewdata
|
||||
|
||||
import com.tangem.domain.staking.model.common.RewardInfo
|
||||
import com.tangem.domain.staking.model.common.RewardType
|
||||
import com.tangem.domain.staking.model.StakingTarget
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.Yield.Validator.ValidatorStatus
|
||||
import com.tangem.domain.staking.model.toStakingTarget
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
|
@ -13,9 +17,9 @@ internal object ValidatorStatePreviewData {
|
|||
status = ValidatorStatus.ACTIVE,
|
||||
name = "Luganodes",
|
||||
image = "https://assets.stakek.it/validators/luganodes.png",
|
||||
rewardInfo = Yield.RewardInfo(
|
||||
rewardInfo = RewardInfo(
|
||||
rate = BigDecimal("0.054823398040640445"),
|
||||
type = Yield.RewardType.APR,
|
||||
type = RewardType.APR,
|
||||
),
|
||||
commission = 0.1,
|
||||
stakedBalance = "355544384.45009977",
|
||||
|
|
@ -29,9 +33,9 @@ internal object ValidatorStatePreviewData {
|
|||
status = ValidatorStatus.ACTIVE,
|
||||
name = "InfStones",
|
||||
image = "https://assets.stakek.it/validators/infstones.png",
|
||||
rewardInfo = Yield.RewardInfo(
|
||||
rewardInfo = RewardInfo(
|
||||
rate = BigDecimal("0.057786472172836965"),
|
||||
type = Yield.RewardType.APR,
|
||||
type = RewardType.APR,
|
||||
),
|
||||
commission = 0.05,
|
||||
stakedBalance = "12495684.05643019",
|
||||
|
|
@ -45,9 +49,9 @@ internal object ValidatorStatePreviewData {
|
|||
status = ValidatorStatus.ACTIVE,
|
||||
name = "Kiln",
|
||||
image = "https://assets.stakek.it/validators/kiln.png",
|
||||
rewardInfo = Yield.RewardInfo(
|
||||
rewardInfo = RewardInfo(
|
||||
rate = BigDecimal("0.057786472172836965"),
|
||||
type = Yield.RewardType.APR,
|
||||
type = RewardType.APR,
|
||||
),
|
||||
commission = 0.05,
|
||||
stakedBalance = "85400369.96393165",
|
||||
|
|
@ -58,11 +62,13 @@ internal object ValidatorStatePreviewData {
|
|||
),
|
||||
)
|
||||
|
||||
private val targetList: List<StakingTarget> = validatorList.map { it.toStakingTarget() }
|
||||
|
||||
val validatorState = StakingStates.ValidatorState.Data(
|
||||
availableValidators = validatorList,
|
||||
chosenValidator = validatorList.first(),
|
||||
availableTargets = targetList,
|
||||
chosenTarget = targetList.first(),
|
||||
isPrimaryButtonEnabled = true,
|
||||
activeValidator = null,
|
||||
activeTarget = null,
|
||||
isClickable = true,
|
||||
isVisibleOnConfirmation = true,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.tangem.features.staking.impl.presentation.state.stub
|
|||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.StakingTarget
|
||||
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
|
||||
import com.tangem.features.staking.impl.presentation.state.BalanceState
|
||||
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
|
||||
|
|
@ -40,7 +40,7 @@ internal object StakingClickIntentsStub : StakingClickIntents {
|
|||
|
||||
override fun openValidators() {}
|
||||
|
||||
override fun onValidatorSelect(validator: Yield.Validator) {}
|
||||
override fun onTargetSelect(target: StakingTarget) {}
|
||||
|
||||
override fun openRewardsValidators() {}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
|||
import com.tangem.domain.models.staking.PendingAction
|
||||
import com.tangem.domain.models.staking.action.StakingActionType
|
||||
import com.tangem.domain.staking.model.StakingApproval
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.StakingIntegration
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.features.staking.impl.presentation.state.*
|
||||
import com.tangem.features.staking.impl.presentation.state.utils.isCompositePendingActions
|
||||
|
|
@ -24,7 +24,7 @@ internal class SetConfirmationStateInitTransformer(
|
|||
private val stakingApproval: StakingApproval,
|
||||
private val stakingAllowance: BigDecimal,
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val yieldArgs: Yield.Args,
|
||||
private val integration: StakingIntegration,
|
||||
private val pendingActions: ImmutableList<PendingAction>? = null,
|
||||
private val pendingAction: PendingAction? = pendingActions?.firstOrNull(),
|
||||
) : Transformer<StakingUiState> {
|
||||
|
|
@ -66,7 +66,7 @@ internal class SetConfirmationStateInitTransformer(
|
|||
}
|
||||
|
||||
private fun getActionType(prevState: StakingUiState): StakingActionCommonType {
|
||||
val isPartialEnterAmountDisabled = yieldArgs.enter.isPartialAmountDisabled
|
||||
val isPartialEnterAmountDisabled = integration.isPartialAmountDisabled
|
||||
val isPartialExitAmountDisabled = isPartiallyUnstakeDisabled(prevState)
|
||||
return when {
|
||||
isEnter -> StakingActionCommonType.Enter(isPartialEnterAmountDisabled)
|
||||
|
|
@ -87,12 +87,12 @@ internal class SetConfirmationStateInitTransformer(
|
|||
|
||||
private fun isPartiallyUnstakeDisabled(state: StakingUiState): Boolean {
|
||||
val isSolana = BlockchainUtils.isSolana(state.cryptoCurrencyBlockchainId)
|
||||
val isValidatorPreferred = balanceState?.validator?.preferred == true
|
||||
val isTargetPreferred = balanceState?.target?.isPreferred == true
|
||||
|
||||
return if (isSolana && !isValidatorPreferred) {
|
||||
return if (isSolana && !isTargetPreferred) {
|
||||
true
|
||||
} else {
|
||||
yieldArgs.exit?.isPartialAmountDisabled == true
|
||||
integration.exitArgs?.isPartialAmountDisabled == true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat
|
|||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.StakingIntegration
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.FeeState
|
||||
|
|
@ -18,7 +18,7 @@ import com.tangem.features.staking.impl.presentation.state.utils.getRewardSchedu
|
|||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class SetConfirmationStateLoadingTransformer(
|
||||
private val yield: Yield,
|
||||
private val integration: StakingIntegration,
|
||||
private val appCurrency: AppCurrency,
|
||||
private val cryptoCurrency: CryptoCurrency,
|
||||
) : Transformer<StakingUiState> {
|
||||
|
|
@ -48,7 +48,7 @@ internal class SetConfirmationStateLoadingTransformer(
|
|||
)
|
||||
}
|
||||
val rewardSchedule = getRewardScheduleText(
|
||||
rewardSchedule = yield.metadata.rewardSchedule,
|
||||
rewardSchedule = integration.rewardSchedule,
|
||||
networkId = cryptoCurrency.network.rawId,
|
||||
decapitalize = true,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ import com.tangem.domain.models.account.Account
|
|||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.staking.BalanceItem
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.staking.model.common.RewardType
|
||||
import com.tangem.domain.staking.model.StakingIntegration
|
||||
import com.tangem.domain.staking.model.StakingTarget
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
|
||||
|
|
@ -41,7 +44,7 @@ import java.math.BigDecimal
|
|||
@Suppress("LongParameterList")
|
||||
internal class SetInitialDataStateTransformer(
|
||||
private val clickIntents: StakingClickIntents,
|
||||
private val yield: Yield,
|
||||
private val integration: StakingIntegration,
|
||||
private val isAnyTokenStaked: Boolean,
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val userWalletProvider: Provider<UserWallet>,
|
||||
|
|
@ -55,7 +58,7 @@ internal class SetInitialDataStateTransformer(
|
|||
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
|
||||
|
||||
private val rewardsValidatorStateConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
RewardsValidatorStateConverter(cryptoCurrencyStatus, appCurrencyProvider, yield)
|
||||
RewardsValidatorStateConverter(cryptoCurrencyStatus, appCurrencyProvider, integration)
|
||||
}
|
||||
|
||||
private val yieldBalancesConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
|
|
@ -63,7 +66,7 @@ internal class SetInitialDataStateTransformer(
|
|||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
balancesToShowProvider = balancesToShowProvider,
|
||||
yield = yield,
|
||||
integration = integration,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -116,27 +119,27 @@ internal class SetInitialDataStateTransformer(
|
|||
}
|
||||
|
||||
private fun createAnnualPercentageItem(): RoundedListWithDividersItemData {
|
||||
val validators = yield.preferredValidators
|
||||
val rateRangeInfo = getPercentageRange(validators)
|
||||
val targets = integration.preferredTargets
|
||||
val rateRangeInfo = getPercentageRange(targets)
|
||||
return RoundedListWithDividersItemData(
|
||||
id = R.string.staking_details_annual_percentage_rate,
|
||||
startText = getRateStartText(rateRangeInfo.first),
|
||||
endText = rateRangeInfo.second,
|
||||
iconClick = {
|
||||
when (rateRangeInfo.first) {
|
||||
Yield.RewardType.APR -> clickIntents.onInfoClick(InfoType.ANNUAL_PERCENTAGE_RATE)
|
||||
Yield.RewardType.APY -> clickIntents.onInfoClick(InfoType.ANNUAL_PERCENTAGE_YIELD)
|
||||
Yield.RewardType.UNKNOWN -> {}
|
||||
RewardType.APR -> clickIntents.onInfoClick(InfoType.ANNUAL_PERCENTAGE_RATE)
|
||||
RewardType.APY -> clickIntents.onInfoClick(InfoType.ANNUAL_PERCENTAGE_YIELD)
|
||||
RewardType.UNKNOWN -> {}
|
||||
}
|
||||
},
|
||||
isEndTextHighlighted = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getRateStartText(rewardType: Yield.RewardType): TextReference {
|
||||
private fun getRateStartText(rewardType: RewardType): TextReference {
|
||||
return when (rewardType) {
|
||||
Yield.RewardType.APR -> TextReference.Res(R.string.staking_details_annual_percentage_rate)
|
||||
Yield.RewardType.APY -> TextReference.Res(R.string.staking_details_annual_percentage_yield)
|
||||
RewardType.APR -> TextReference.Res(R.string.staking_details_annual_percentage_rate)
|
||||
RewardType.APY -> TextReference.Res(R.string.staking_details_annual_percentage_yield)
|
||||
else -> TextReference.EMPTY
|
||||
}
|
||||
}
|
||||
|
|
@ -153,7 +156,7 @@ internal class SetInitialDataStateTransformer(
|
|||
}
|
||||
|
||||
private fun createUnbondingPeriodItem(): RoundedListWithDividersItemData? {
|
||||
val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days ?: return null
|
||||
val cooldownPeriodDays = integration.cooldownPeriodDays ?: return null
|
||||
return RoundedListWithDividersItemData(
|
||||
id = R.string.staking_details_unbonding_period,
|
||||
startText = TextReference.Res(R.string.staking_details_unbonding_period),
|
||||
|
|
@ -169,7 +172,7 @@ internal class SetInitialDataStateTransformer(
|
|||
private fun createMinimumRequirementItem(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
): RoundedListWithDividersItemData? {
|
||||
val minimumCryptoAmount = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum ?: return null
|
||||
val minimumCryptoAmount = integration.enterMinimumAmount ?: return null
|
||||
val blockchainId = cryptoCurrencyStatus.currency.network.rawId
|
||||
if (!showMinimumRequirementInfo(blockchainId)) return null
|
||||
|
||||
|
|
@ -183,7 +186,7 @@ internal class SetInitialDataStateTransformer(
|
|||
}
|
||||
|
||||
private fun createRewardClaimingItem(): RoundedListWithDividersItemData? {
|
||||
val rewardClaiming = yield.metadata.rewardClaiming
|
||||
val rewardClaiming = integration.rewardClaiming
|
||||
val endTextId = rewardClaimingResources[rewardClaiming] ?: return null
|
||||
|
||||
return RoundedListWithDividersItemData(
|
||||
|
|
@ -195,7 +198,7 @@ internal class SetInitialDataStateTransformer(
|
|||
}
|
||||
|
||||
private fun createWarmupPeriodItem(): RoundedListWithDividersItemData? {
|
||||
val warmupPeriodDays = yield.metadata.warmupPeriod.days
|
||||
val warmupPeriodDays = integration.warmupPeriodDays
|
||||
if (warmupPeriodDays == 0) return null
|
||||
|
||||
return RoundedListWithDividersItemData(
|
||||
|
|
@ -212,7 +215,7 @@ internal class SetInitialDataStateTransformer(
|
|||
|
||||
private fun createRewardScheduleItem(): RoundedListWithDividersItemData? {
|
||||
val endTextReference = getRewardScheduleText(
|
||||
rewardSchedule = yield.metadata.rewardSchedule,
|
||||
rewardSchedule = integration.rewardSchedule,
|
||||
networkId = cryptoCurrencyStatus.currency.network.rawId,
|
||||
decapitalize = false,
|
||||
) ?: return null
|
||||
|
|
@ -252,15 +255,19 @@ internal class SetInitialDataStateTransformer(
|
|||
)
|
||||
}
|
||||
|
||||
private fun getPercentageRange(validators: List<Yield.Validator>): Pair<Yield.RewardType, TextReference> {
|
||||
if (validators.isEmpty()) {
|
||||
return Yield.RewardType.APR to stringReference(DASH_SIGN)
|
||||
private fun getPercentageRange(targets: List<StakingTarget>): Pair<RewardType, TextReference> {
|
||||
if (targets.isEmpty()) {
|
||||
return RewardType.APR to stringReference(DASH_SIGN)
|
||||
}
|
||||
val rewardInfos = validators
|
||||
.filter { it.preferred }
|
||||
val rewardInfos = targets
|
||||
.filter { it.isPreferred }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.mapNotNull { it.rewardInfo }
|
||||
?: validators.mapNotNull { it.rewardInfo }
|
||||
?: targets.mapNotNull { it.rewardInfo }
|
||||
|
||||
if (rewardInfos.isEmpty()) {
|
||||
return RewardType.APR to stringReference(DASH_SIGN)
|
||||
}
|
||||
|
||||
val infoWithMinRate = rewardInfos.minBy { it.rate }
|
||||
val infoWithMaxRate = rewardInfos.maxBy { it.rate }
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter
|
|||
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.StakingIntegration
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
|
@ -13,7 +13,7 @@ internal class AmountChangeStateTransformer(
|
|||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val minimumTransactionAmount: EnterAmountBoundary?,
|
||||
private val value: String,
|
||||
private val yield: Yield,
|
||||
private val integration: StakingIntegration,
|
||||
) : Transformer<StakingUiState> {
|
||||
|
||||
private val maxEnterAmountConverter = MaxEnterAmountConverter()
|
||||
|
|
@ -41,7 +41,7 @@ internal class AmountChangeStateTransformer(
|
|||
amountState = AmountRequirementStateTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
maxAmount = maxEnterAmount,
|
||||
yield = yield,
|
||||
integration = integration,
|
||||
actionType = prevState.actionType,
|
||||
).transform(updatedAmountState),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter
|
|||
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldSetMaxAmountTransformer
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.StakingIntegration
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
|
@ -13,7 +13,7 @@ internal class AmountMaxValueStateTransformer(
|
|||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val minimumTransactionAmount: EnterAmountBoundary?,
|
||||
private val actionType: StakingActionCommonType,
|
||||
private val yield: Yield,
|
||||
private val integration: StakingIntegration,
|
||||
) : Transformer<StakingUiState> {
|
||||
|
||||
private val maxEnterAmountConverter = MaxEnterAmountConverter()
|
||||
|
|
@ -38,7 +38,7 @@ internal class AmountMaxValueStateTransformer(
|
|||
amountState = AmountRequirementStateTransformer(
|
||||
maxAmount = maxEnterAmount,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
yield = yield,
|
||||
integration = integration,
|
||||
actionType = prevState.actionType,
|
||||
).transform(updatedAmountState),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto
|
|||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.staking.model.StakingIntegration
|
||||
import com.tangem.domain.staking.model.stakekit.AddressArgument
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
|
|
@ -26,7 +27,7 @@ import java.math.RoundingMode
|
|||
internal class AmountRequirementStateTransformer(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val maxAmount: EnterAmountBoundary,
|
||||
private val yield: Yield,
|
||||
private val integration: StakingIntegration,
|
||||
private val actionType: StakingActionCommonType,
|
||||
) : Transformer<AmountState> {
|
||||
override fun transform(prevState: AmountState): AmountState {
|
||||
|
|
@ -96,11 +97,11 @@ internal class AmountRequirementStateTransformer(
|
|||
|
||||
return when (actionType) {
|
||||
is StakingActionCommonType.Enter -> {
|
||||
val enterRequirements = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]
|
||||
val enterRequirements = integration.enterArgs?.args?.get(Yield.Args.ArgType.AMOUNT)
|
||||
enterRequirements?.getError(amountDecimal, R.string.staking_amount_requirement_error)
|
||||
}
|
||||
is StakingActionCommonType.Exit -> {
|
||||
val exitRequirements = yield.args.exit?.args?.get(Yield.Args.ArgType.AMOUNT)
|
||||
val exitRequirements = integration.exitArgs?.args?.get(Yield.Args.ArgType.AMOUNT)
|
||||
exitRequirements?.getError(amountDecimal, R.string.staking_unstake_amount_requirement_error)
|
||||
}
|
||||
else -> null
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ internal class ShowApprovalBottomSheetTransformer(
|
|||
val fee = feeState.fee ?: return prevState
|
||||
|
||||
val walletAddress = cryptoCurrencyValue.networkAddress?.defaultAddress?.value.orEmpty()
|
||||
val validatorAddress = validatorState.chosenValidator.address
|
||||
val targetAddress = validatorState.chosenTarget.address
|
||||
val feeCryptoValue = fee.amount.value.format {
|
||||
crypto(fee.amount.currencySymbol, fee.amount.decimals)
|
||||
}
|
||||
|
|
@ -57,7 +57,7 @@ internal class ShowApprovalBottomSheetTransformer(
|
|||
amount = amountState.amountTextField.value,
|
||||
approveType = ApproveType.UNLIMITED,
|
||||
walletAddress = walletAddress,
|
||||
spenderAddress = validatorAddress,
|
||||
spenderAddress = targetAddress,
|
||||
fee = resourceReference(
|
||||
R.string.common_crypto_fiat_format,
|
||||
wrappedList(feeCryptoValue, feeFiatValue),
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@ import com.tangem.core.ui.extensions.stringReference
|
|||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.staking.model.StakingIntegration
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import com.tangem.domain.staking.model.stakekit.StakingErrors
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
|
||||
|
|
@ -47,12 +47,12 @@ internal class AddStakingNotificationsTransformer(
|
|||
private val stakingError: StakingError?,
|
||||
private val currencyCheck: CryptoCurrencyCheck,
|
||||
private val isSubtractAvailable: Boolean,
|
||||
private val yield: Yield,
|
||||
private val integration: StakingIntegration,
|
||||
) : Transformer<StakingUiState> {
|
||||
|
||||
private val stakingInfoNotificationsFactory = StakingInfoNotificationsFactory(
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
yield = yield,
|
||||
integration = integration,
|
||||
isSubtractAvailable = isSubtractAvailable,
|
||||
)
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ internal class AddStakingNotificationsTransformer(
|
|||
isSubtractAvailable = isSubtractAvailable,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
val minimumRequirement = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum.orZero()
|
||||
val minimumRequirement = integration.enterMinimumAmount.orZero()
|
||||
val sendingAmount = if (isEnterAction) {
|
||||
checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isSubtractAvailable,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
|||
import com.tangem.domain.models.staking.BalanceType
|
||||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.models.staking.action.StakingActionType
|
||||
import com.tangem.domain.staking.model.StakingIntegration
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.features.staking.impl.R
|
||||
|
|
@ -26,7 +27,7 @@ import java.math.BigDecimal
|
|||
|
||||
internal class StakingInfoNotificationsFactory(
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val yield: Yield,
|
||||
private val integration: StakingIntegration,
|
||||
private val isSubtractAvailable: Boolean,
|
||||
) {
|
||||
|
||||
|
|
@ -94,7 +95,7 @@ internal class StakingInfoNotificationsFactory(
|
|||
resourceReference(R.string.staking_notification_withdraw_text)
|
||||
}
|
||||
StakingActionType.UNLOCK_LOCKED -> {
|
||||
val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days
|
||||
val cooldownPeriodDays = integration.cooldownPeriodDays
|
||||
if (cooldownPeriodDays != null) {
|
||||
resourceReference(R.string.staking_unlocked_locked) to resourceReference(
|
||||
R.string.staking_notification_unlock_text,
|
||||
|
|
@ -213,7 +214,7 @@ internal class StakingInfoNotificationsFactory(
|
|||
if (prevState.actionType !is StakingActionCommonType.Exit) return
|
||||
|
||||
val maxAmount = prevState.balanceState?.cryptoAmount ?: return
|
||||
val exitRequirements = yield.args.exit?.args?.get(Yield.Args.ArgType.AMOUNT) ?: return
|
||||
val exitRequirements = integration.exitArgs?.args?.get(Yield.Args.ArgType.AMOUNT) ?: return
|
||||
|
||||
val amountLeft = maxAmount - actionAmount
|
||||
val isNotEnoughLeft = !amountLeft.isZero() && amountLeft < exitRequirements.minimum.orZero()
|
||||
|
|
@ -224,7 +225,7 @@ internal class StakingInfoNotificationsFactory(
|
|||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.addUnstakeInfoNotification() {
|
||||
val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days
|
||||
val cooldownPeriodDays = integration.cooldownPeriodDays
|
||||
|
||||
val cryptoCurrencyNetworkIdValue = cryptoCurrencyStatusProvider().currency.network.rawId
|
||||
if (cooldownPeriodDays != null) {
|
||||
|
|
@ -248,18 +249,17 @@ internal class StakingInfoNotificationsFactory(
|
|||
val initialInfoState = prevState.initialInfoState as? StakingStates.InitialInfoState.Data
|
||||
val stakingBalances = (initialInfoState?.yieldBalance as? InnerYieldBalanceState.Data)?.balances
|
||||
|
||||
val validatorAddress = prevState.balanceState?.validator?.address ?: return
|
||||
val targetAddress = prevState.balanceState?.target?.address ?: return
|
||||
|
||||
val stakesCountWithCertainValidator = stakingBalances.orEmpty()
|
||||
.filter {
|
||||
it.type == BalanceType.STAKED ||
|
||||
it.type == BalanceType.PREPARING ||
|
||||
it.type == BalanceType.UNSTAKED
|
||||
val stakesCountWithCertainTarget = stakingBalances.orEmpty()
|
||||
.count { state ->
|
||||
(state.type == BalanceType.STAKED ||
|
||||
state.type == BalanceType.PREPARING ||
|
||||
state.type == BalanceType.UNSTAKED) &&
|
||||
state.target?.address == targetAddress
|
||||
}
|
||||
.filter { it.validator?.address == validatorAddress }
|
||||
.size
|
||||
|
||||
if (stakesCountWithCertainValidator > 1) {
|
||||
if (stakesCountWithCertainTarget > 1) {
|
||||
add(
|
||||
StakingNotification.Info.Ordinary(
|
||||
title = resourceReference(R.string.staking_notification_ton_have_to_unstake_all_title),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers.validator
|
||||
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.StakingIntegration
|
||||
import com.tangem.domain.staking.model.StakingTarget
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.models.staking.action.StakingActionType
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
|
|
@ -9,8 +10,8 @@ import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
|||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class ValidatorSelectChangeTransformer(
|
||||
private val yield: Yield,
|
||||
private val selectedValidator: Yield.Validator?,
|
||||
private val integration: StakingIntegration,
|
||||
private val selectedTarget: StakingTarget?,
|
||||
) : Transformer<StakingUiState> {
|
||||
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
|
|
@ -23,27 +24,27 @@ internal class ValidatorSelectChangeTransformer(
|
|||
val isFromInfoScreen = prevState.currentStep == StakingStep.InitialInfo
|
||||
val isVoteLocked = confirmationState?.pendingAction?.type == StakingActionType.VOTE_LOCKED
|
||||
|
||||
val activeValidator = selectedValidator.takeIf { isFromInfoScreen && isRestake }
|
||||
?: validatorState?.activeValidator
|
||||
val filteredValidators = yield.preferredValidators.filterNot { it == activeValidator }
|
||||
val activeTarget = selectedTarget.takeIf { isFromInfoScreen && isRestake }
|
||||
?: validatorState?.activeTarget
|
||||
val filteredTargets = integration.preferredTargets.filterNot { it == activeTarget }
|
||||
|
||||
val selectedValidator = if (isRestake && isFromInfoScreen) {
|
||||
filteredValidators.firstOrNull()
|
||||
val selectedTarget = if (isRestake && isFromInfoScreen) {
|
||||
filteredTargets.firstOrNull()
|
||||
} else {
|
||||
selectedValidator
|
||||
selectedTarget
|
||||
}
|
||||
|
||||
if (selectedValidator == null && yield.preferredValidators.isEmpty()) {
|
||||
if (selectedTarget == null && integration.preferredTargets.isEmpty()) {
|
||||
return prevState
|
||||
}
|
||||
|
||||
return prevState.copy(
|
||||
validatorState = StakingStates.ValidatorState.Data(
|
||||
chosenValidator = selectedValidator ?: yield.preferredValidators.first(),
|
||||
availableValidators = filteredValidators,
|
||||
chosenTarget = selectedTarget ?: integration.preferredTargets.first(),
|
||||
availableTargets = filteredTargets,
|
||||
isPrimaryButtonEnabled = true,
|
||||
isClickable = yield.preferredValidators.size > 1,
|
||||
activeValidator = activeValidator,
|
||||
isClickable = integration.preferredTargets.size > 1,
|
||||
activeTarget = activeTarget,
|
||||
isVisibleOnConfirmation = isEnter || isRestake || isVoteLocked,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.utils
|
||||
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.domain.staking.model.common.RewardType
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.utils.StakingRewardSchedule.COSMOS_SCHEDULE
|
||||
|
|
@ -52,18 +53,18 @@ internal fun getRewardScheduleText(
|
|||
}
|
||||
}
|
||||
|
||||
internal fun getRewardTypeShortText(rewardType: Yield.RewardType): TextReference {
|
||||
internal fun getRewardTypeShortText(rewardType: RewardType): TextReference {
|
||||
return when (rewardType) {
|
||||
Yield.RewardType.APR -> TextReference.Res(R.string.staking_details_apr)
|
||||
Yield.RewardType.APY -> TextReference.Res(R.string.staking_details_apy)
|
||||
RewardType.APR -> TextReference.Res(R.string.staking_details_apr)
|
||||
RewardType.APY -> TextReference.Res(R.string.staking_details_apy)
|
||||
else -> TextReference.EMPTY
|
||||
}
|
||||
}
|
||||
|
||||
internal fun getRewardTypeLongText(rewardType: Yield.RewardType): TextReference {
|
||||
internal fun getRewardTypeLongText(rewardType: RewardType): TextReference {
|
||||
return when (rewardType) {
|
||||
Yield.RewardType.APR -> TextReference.Res(R.string.staking_details_annual_percentage_rate)
|
||||
Yield.RewardType.APY -> TextReference.Res(R.string.staking_details_annual_percentage_yield)
|
||||
RewardType.APR -> TextReference.Res(R.string.staking_details_annual_percentage_rate)
|
||||
RewardType.APY -> TextReference.Res(R.string.staking_details_annual_percentage_yield)
|
||||
else -> TextReference.EMPTY
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import com.tangem.core.ui.extensions.*
|
|||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.common.RewardType
|
||||
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
|
||||
import com.tangem.features.staking.impl.presentation.state.BalanceState
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
|
|
@ -30,7 +30,7 @@ internal fun StakingClaimRewardsValidatorContent(
|
|||
) {
|
||||
if (state !is StakingStates.RewardsValidatorsState.Data) return
|
||||
Column(
|
||||
modifier = Modifier // Do not put fillMaxSize() in here
|
||||
modifier = modifier // This function shouldn't itself apply fillMaxSize(); callers should avoid passing it here
|
||||
.background(TangemTheme.colors.background.secondary)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing12)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
|
|
@ -42,9 +42,9 @@ internal fun StakingClaimRewardsValidatorContent(
|
|||
caption = item.getAprTextNeutral(),
|
||||
infoTitle = item.formattedFiatAmount,
|
||||
infoSubtitle = item.formattedCryptoAmount,
|
||||
imageUrl = item.validator?.image.orEmpty(),
|
||||
imageUrl = item.target?.image.orEmpty(),
|
||||
onImageError = { ValidatorImagePlaceholder() },
|
||||
modifier = modifier
|
||||
modifier = Modifier
|
||||
.roundedShapeItemDecoration(index, state.rewards.lastIndex, false)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(
|
||||
|
|
@ -65,11 +65,11 @@ internal fun StakingClaimRewardsValidatorContent(
|
|||
@Suppress("UnusedPrivateMember")
|
||||
@Composable
|
||||
private fun BalanceState.getAprTextColored() = combinedReference(
|
||||
getRewardTypeShortText(validator?.rewardInfo?.type ?: Yield.RewardType.UNKNOWN),
|
||||
getRewardTypeShortText(target?.rewardInfo?.type ?: RewardType.UNKNOWN),
|
||||
annotatedReference {
|
||||
appendSpace()
|
||||
appendColored(
|
||||
text = validator?.rewardInfo?.rate?.orZero().format { percent() },
|
||||
text = target?.rewardInfo?.rate?.orZero().format { percent() },
|
||||
color = TangemTheme.colors.text.accent,
|
||||
)
|
||||
},
|
||||
|
|
@ -77,6 +77,6 @@ private fun BalanceState.getAprTextColored() = combinedReference(
|
|||
|
||||
@Composable
|
||||
private fun BalanceState.getAprTextNeutral() = combinedReference(
|
||||
getRewardTypeShortText(validator?.rewardInfo?.type ?: Yield.RewardType.UNKNOWN),
|
||||
stringReference(" " + validator?.rewardInfo?.rate?.orZero().format { percent() }),
|
||||
getRewardTypeShortText(target?.rewardInfo?.type ?: RewardType.UNKNOWN),
|
||||
stringReference(" " + target?.rewardInfo?.rate?.orZero().format { percent() }),
|
||||
)
|
||||
|
|
@ -46,7 +46,7 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
import com.tangem.core.ui.test.StakingDetailsScreenTestTags
|
||||
import com.tangem.domain.models.staking.BalanceType
|
||||
import com.tangem.domain.models.staking.RewardBlockType
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.common.RewardType
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
|
||||
import com.tangem.features.staking.impl.presentation.state.BalanceState
|
||||
|
|
@ -317,11 +317,11 @@ private fun StakeButtonBlock(buttonState: NavigationButtonsState) {
|
|||
@Suppress("UnusedPrivateMember")
|
||||
@Composable
|
||||
private fun BalanceState.getAprTextColored() = combinedReference(
|
||||
getRewardTypeShortText(validator?.rewardInfo?.type ?: Yield.RewardType.UNKNOWN),
|
||||
getRewardTypeShortText(target?.rewardInfo?.type ?: RewardType.UNKNOWN),
|
||||
annotatedReference {
|
||||
appendSpace()
|
||||
appendColored(
|
||||
text = validator?.rewardInfo?.rate?.orZero().format { percent() },
|
||||
text = target?.rewardInfo?.rate?.orZero().format { percent() },
|
||||
color = TangemTheme.colors.text.accent,
|
||||
)
|
||||
},
|
||||
|
|
@ -329,8 +329,8 @@ private fun BalanceState.getAprTextColored() = combinedReference(
|
|||
|
||||
@Composable
|
||||
private fun BalanceState.getAprTextNeutral() = combinedReference(
|
||||
getRewardTypeShortText(validator?.rewardInfo?.type ?: Yield.RewardType.UNKNOWN),
|
||||
stringReference(" " + validator?.rewardInfo?.rate?.orZero().format { percent() }),
|
||||
getRewardTypeShortText(target?.rewardInfo?.type ?: RewardType.UNKNOWN),
|
||||
stringReference(" " + target?.rewardInfo?.rate?.orZero().format { percent() }),
|
||||
)
|
||||
|
||||
@Composable
|
||||
|
|
@ -347,7 +347,7 @@ private fun BalanceState.getImage() = when (type) {
|
|||
BalanceType.UNSTAKED,
|
||||
BalanceType.LOCKED,
|
||||
-> null
|
||||
else -> validator?.image
|
||||
else -> target?.image
|
||||
}
|
||||
|
||||
private val textGradientColors = listOf(
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue