Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-30 12:29:17 +05:00
commit 1bf78a1f1a
13 changed files with 159 additions and 61 deletions

View file

@ -22,6 +22,7 @@ dependencies {
implementation(projects.domain.tokens.models)
implementation(projects.domain.staking)
implementation(projects.domain.wallets.models)
implementation(projects.domain.legacy)
/** Feature Api modules */
implementation(projects.features.staking.api)
@ -40,6 +41,8 @@ dependencies {
implementation(projects.libs.blockchainSdk)
implementation(deps.tangem.card.core)
implementation(deps.tangem.blockchain) {
exclude(module = "joda-time")
}

View file

@ -1,8 +1,10 @@
package com.tangem.data.staking
import android.util.Base64
import arrow.core.raise.catch
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.common.extensions.toCompressedPublicKey
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.staking.converters.*
import com.tangem.data.staking.converters.action.ActionStatusConverter
@ -23,7 +25,10 @@ import com.tangem.datasource.local.token.StakingBalanceStore
import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.lce.lceFlow
import com.tangem.domain.staking.model.*
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata
import com.tangem.domain.staking.model.stakekit.NetworkType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.staking.model.stakekit.YieldBalanceList
@ -37,10 +42,10 @@ import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.toFormattedString
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -55,6 +60,7 @@ internal class DefaultStakingRepository(
private val cacheRegistry: CacheRegistry,
private val dispatchers: CoroutineDispatcherProvider,
private val stakingFeatureToggle: StakingFeatureToggles,
private val walletManagersFacade: WalletManagersFacade,
) : StakingRepository {
private val stakingNetworkTypeConverter = StakingNetworkTypeConverter()
@ -160,11 +166,27 @@ internal class DefaultStakingRepository(
}
}
override suspend fun createAction(params: ActionParams): StakingAction {
override suspend fun createAction(
userWalletId: UserWalletId,
network: Network,
params: ActionParams,
): StakingAction {
return withContext(dispatchers.io) {
val response = when (params.actionCommonType) {
StakingActionCommonType.ENTER -> stakeKitApi.createEnterAction(createActionRequestBody(params))
StakingActionCommonType.EXIT -> stakeKitApi.createExitAction(createActionRequestBody(params))
StakingActionCommonType.ENTER -> stakeKitApi.createEnterAction(
createActionRequestBody(
userWalletId,
network,
params,
),
)
StakingActionCommonType.EXIT -> stakeKitApi.createExitAction(
createActionRequestBody(
userWalletId,
network,
params,
),
)
StakingActionCommonType.PENDING -> stakeKitApi.createPendingAction(
createPendingActionRequestBody(params),
)
@ -174,11 +196,27 @@ internal class DefaultStakingRepository(
}
}
override suspend fun estimateGas(params: ActionParams): StakingGasEstimate {
override suspend fun estimateGas(
userWalletId: UserWalletId,
network: Network,
params: ActionParams,
): StakingGasEstimate {
return withContext(dispatchers.io) {
val gasEstimateDTO = when (params.actionCommonType) {
StakingActionCommonType.ENTER -> stakeKitApi.estimateGasOnEnter(createActionRequestBody(params))
StakingActionCommonType.EXIT -> stakeKitApi.estimateGasOnExit(createActionRequestBody(params))
StakingActionCommonType.ENTER -> stakeKitApi.estimateGasOnEnter(
createActionRequestBody(
userWalletId,
network,
params,
),
)
StakingActionCommonType.EXIT -> stakeKitApi.estimateGasOnExit(
createActionRequestBody(
userWalletId,
network,
params,
),
)
StakingActionCommonType.PENDING -> stakeKitApi.estimateGasOnPending(
createPendingActionRequestBody(params),
)
@ -435,12 +473,19 @@ internal class DefaultStakingRepository(
}
}
private fun createActionRequestBody(params: ActionParams): ActionRequestBody {
private suspend fun createActionRequestBody(
userWalletId: UserWalletId,
network: Network,
params: ActionParams,
): ActionRequestBody {
return ActionRequestBody(
integrationId = params.integrationId,
addresses = Address(params.address),
addresses = Address(
address = params.address,
additionalAddresses = createAdditionalAddresses(userWalletId, network, params),
),
args = ActionRequestBodyArgs(
amount = params.amount.toFormattedString(params.token.decimals),
amount = params.amount.toPlainString(),
inputToken = tokenConverter.convertBack(params.token),
validatorAddress = params.validatorAddress,
),
@ -453,12 +498,29 @@ internal class DefaultStakingRepository(
type = params.type ?: StakingActionType.UNKNOWN,
passthrough = params.passthrough.orEmpty(),
args = ActionRequestBodyArgs(
amount = params.amount.toFormattedString(params.token.decimals),
amount = params.amount.toPlainString(),
validatorAddress = params.validatorAddress,
),
)
}
private suspend fun createAdditionalAddresses(
userWalletId: UserWalletId,
network: Network,
params: ActionParams,
): Address.AdditionalAddresses? {
val selectedWallet = walletManagersFacade.getOrCreateWalletManager(userWalletId, network)
return when (params.token.network) {
NetworkType.COSMOS -> Address.AdditionalAddresses(
cosmosPubKey = Base64.encodeToString(
/* input = */ selectedWallet?.wallet?.publicKey?.blockchainKey?.toCompressedPublicKey(),
/* flags = */ Base64.NO_WRAP,
),
)
else -> null
}
}
override fun isStakeMoreAvailable(networkId: Network.ID): Boolean {
val blockchain = Blockchain.fromId(networkId.value)
return when (blockchain) {

View file

@ -13,6 +13,7 @@ import com.tangem.datasource.local.token.StakingBalanceStore
import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -35,6 +36,7 @@ internal object StakingDataModule {
dispatchers: CoroutineDispatcherProvider,
stakingFeatureToggle: StakingFeatureToggles,
cacheRegistry: CacheRegistry,
walletManagersFacade: WalletManagersFacade,
): StakingRepository {
return DefaultStakingRepository(
stakeKitApi = stakeKitApi,
@ -44,6 +46,7 @@ internal object StakingDataModule {
dispatchers = dispatchers,
cacheRegistry = cacheRegistry,
stakingFeatureToggle = stakingFeatureToggle,
walletManagersFacade = walletManagersFacade,
)
}

View file

@ -12,6 +12,7 @@ data class ActionParams(
val address: String,
val validatorAddress: String,
val token: Token,
val publicKey: String? = null,
val passthrough: String? = null,
val type: StakingActionType? = null,
)

View file

@ -6,6 +6,8 @@ import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
/**
* Use case for staking gas estimation.
@ -15,9 +17,13 @@ class EstimateGasUseCase(
private val stakingErrorResolver: StakingErrorResolver,
) {
suspend operator fun invoke(params: ActionParams): Either<StakingError, StakingGasEstimate> {
suspend operator fun invoke(
userWalletId: UserWalletId,
network: Network,
params: ActionParams,
): Either<StakingError, StakingGasEstimate> {
return Either.catch {
stakingRepository.estimateGas(params)
stakingRepository.estimateGas(userWalletId, network, params)
}.mapLeft {
stakingErrorResolver.resolve(it)
}

View file

@ -6,6 +6,8 @@ import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.delay
/**
@ -16,9 +18,13 @@ class GetStakingTransactionUseCase(
private val stakingErrorResolver: StakingErrorResolver,
) {
suspend operator fun invoke(params: ActionParams): Either<StakingError, StakingTransaction> {
suspend operator fun invoke(
userWalletId: UserWalletId,
network: Network,
params: ActionParams,
): Either<StakingError, StakingTransaction> {
return Either.catch {
val createAction = stakingRepository.createAction(params)
val createAction = stakingRepository.createAction(userWalletId, network, params)
// workaround, sometimes transaction is not created immediately after actions/enter
delay(PATCH_TRANSACTION_REQUEST_DELAY)

View file

@ -61,9 +61,9 @@ interface StakingRepository {
addresses: List<CryptoCurrencyAddress>,
): YieldBalanceList
suspend fun createAction(params: ActionParams): StakingAction
suspend fun createAction(userWalletId: UserWalletId, network: Network, params: ActionParams): StakingAction
suspend fun estimateGas(params: ActionParams): StakingGasEstimate
suspend fun estimateGas(userWalletId: UserWalletId, network: Network, params: ActionParams): StakingGasEstimate
suspend fun constructTransaction(transactionId: String): StakingTransaction

View file

@ -167,7 +167,11 @@ class MockStakingRepository : StakingRepository {
balances = listOf(YieldBalance.Error),
)
override suspend fun createAction(params: ActionParams): StakingAction {
override suspend fun createAction(
userWalletId: UserWalletId,
network: Network,
params: ActionParams,
): StakingAction {
return StakingAction(
id = "quis",
integrationId = "persequeris",
@ -182,7 +186,11 @@ class MockStakingRepository : StakingRepository {
)
}
override suspend fun estimateGas(params: ActionParams): StakingGasEstimate {
override suspend fun estimateGas(
userWalletId: UserWalletId,
network: Network,
params: ActionParams,
): StakingGasEstimate {
return StakingGasEstimate(
amount = BigDecimal(0.0001),
token = Token(

View file

@ -113,7 +113,6 @@ internal class YieldBalancesConverter(
private fun BalanceType.toGroup() = when (this) {
BalanceType.PREPARING,
BalanceType.STAKED,
BalanceType.REWARDS,
BalanceType.AVAILABLE,
BalanceType.LOCKED,
-> BalanceGroupType.ACTIVE
@ -121,6 +120,7 @@ internal class YieldBalancesConverter(
BalanceType.UNLOCKING,
BalanceType.UNSTAKED,
-> BalanceGroupType.UNSTAKED
BalanceType.REWARDS,
BalanceType.UNKNOWN,
-> BalanceGroupType.UNKNOWN
}

View file

@ -12,13 +12,11 @@ internal class AmountChangeStateTransformer(
private val value: String,
) : Transformer<StakingUiState> {
private val amountRequirementStateTransformer by lazy(LazyThreadSafetyMode.NONE) {
AmountRequirementStateTransformer(
cryptoCurrencyStatus,
yield,
value,
)
}
private val amountRequirementStateTransformer = AmountRequirementStateTransformer(
cryptoCurrencyStatus,
yield,
value,
)
override fun transform(prevState: StakingUiState): StakingUiState {
val updatedAmountState = AmountFieldChangeTransformer(

View file

@ -12,16 +12,13 @@ internal class AmountMaxValueStateTransformer(
private val yield: Yield,
) : Transformer<StakingUiState> {
private val amountRequirementStateTransformer by lazy(LazyThreadSafetyMode.NONE) {
val value = cryptoCurrencyStatus.value.amount
private val amountRequirementStateTransformer = AmountRequirementStateTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
yield = yield,
value = cryptoCurrencyStatus.value.amount
?.parseBigDecimal(cryptoCurrencyStatus.currency.decimals)
.orEmpty()
AmountRequirementStateTransformer(
cryptoCurrencyStatus,
yield,
value,
)
}
.orEmpty(),
)
override fun transform(prevState: StakingUiState): StakingUiState {
val updatedAmountState = AmountFieldMaxAmountTransformer(cryptoCurrencyStatus).transform(prevState.amountState)

View file

@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.staking.model.stakekit.AddressArgument
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.R
@ -18,38 +19,47 @@ internal class AmountRequirementStateTransformer(
) : Transformer<AmountState> {
override fun transform(prevState: AmountState): AmountState {
val amountRequirements = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]
val amountDecimal = value.parseToBigDecimal(cryptoCurrencyStatus.currency.decimals)
return if (prevState !is AmountState.Data || amountRequirements == null) {
prevState
} else {
val isAlreadyErrorState = prevState.amountTextField.isError
val isAmountRequired = amountRequirements.required
val isAmountZero = amountDecimal.isZero()
val isExceedsRequirements =
amountRequirements.maximum?.compareTo(amountDecimal) == -1 ||
amountRequirements.minimum?.compareTo(amountDecimal) == 1
updateWithError(prevState, amountRequirements)
}
}
val isRequirementError = !isAmountZero && isAmountRequired && isExceedsRequirements && !isAlreadyErrorState
if (isRequirementError) {
prevState.copy(
amountTextField = prevState.amountTextField.copy(
isError = true,
error = resourceReference(
R.string.staking_amount_requirement_error,
wrappedList(
BigDecimalFormatter.formatCryptoAmount(
amountRequirements.minimum,
cryptoCurrencyStatus.currency.symbol,
cryptoCurrencyStatus.currency.decimals,
),
private fun updateWithError(prevState: AmountState.Data, amountRequirements: AddressArgument): AmountState {
val isRequirementError = isRequirementError(prevState, amountRequirements)
return if (isRequirementError) {
prevState.copy(
amountTextField = prevState.amountTextField.copy(
isError = true,
error = resourceReference(
R.string.staking_amount_requirement_error,
wrappedList(
BigDecimalFormatter.formatCryptoAmount(
amountRequirements.minimum,
cryptoCurrencyStatus.currency.symbol,
cryptoCurrencyStatus.currency.decimals,
),
),
),
)
} else {
prevState
}
),
)
} else {
prevState
}
}
private fun isRequirementError(prevState: AmountState.Data, amountRequirements: AddressArgument): Boolean {
val amountDecimal = value.parseToBigDecimal(cryptoCurrencyStatus.currency.decimals)
val isAlreadyErrorState = prevState.amountTextField.isError
val isAmountRequired = amountRequirements.required
val isAmountZero = amountDecimal.isZero()
val isExceedsRequirements =
amountRequirements.maximum?.compareTo(amountDecimal) == -1 ||
amountRequirements.minimum?.compareTo(amountDecimal) == 1
return !isAmountZero && isAmountRequired && isExceedsRequirements && !isAlreadyErrorState
}
}

View file

@ -120,6 +120,8 @@ internal class StakingViewModel @Inject constructor(
val pendingActions = confirmationState.pendingActions
val stakingTransaction = getStakingTransactionUseCase(
userWalletId = userWalletId,
network = cryptoCurrencyStatus.currency.network,
params = ActionParams(
actionCommonType = getStakingCommonType(),
integrationId = yield.id,
@ -158,6 +160,8 @@ internal class StakingViewModel @Inject constructor(
val cryptoCurrencyValue = cryptoCurrencyStatus.value
val stakingGasEstimate = estimateGasUseCase(
userWalletId = userWalletId,
network = cryptoCurrencyStatus.currency.network,
params = ActionParams(
actionCommonType = getStakingCommonType(),
integrationId = yield.id,