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

View file

@ -1,8 +1,10 @@
package com.tangem.data.staking package com.tangem.data.staking
import android.util.Base64
import arrow.core.raise.catch import arrow.core.raise.catch
import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.common.extensions.toCompressedPublicKey
import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.staking.converters.* import com.tangem.data.staking.converters.*
import com.tangem.data.staking.converters.action.ActionStatusConverter 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.datasource.local.token.StakingYieldsStore
import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.lce.LceFlow
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.Yield
import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.staking.model.stakekit.YieldBalanceList 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.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.toFormattedString
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -55,6 +60,7 @@ internal class DefaultStakingRepository(
private val cacheRegistry: CacheRegistry, private val cacheRegistry: CacheRegistry,
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
private val stakingFeatureToggle: StakingFeatureToggles, private val stakingFeatureToggle: StakingFeatureToggles,
private val walletManagersFacade: WalletManagersFacade,
) : StakingRepository { ) : StakingRepository {
private val stakingNetworkTypeConverter = StakingNetworkTypeConverter() 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) { return withContext(dispatchers.io) {
val response = when (params.actionCommonType) { val response = when (params.actionCommonType) {
StakingActionCommonType.ENTER -> stakeKitApi.createEnterAction(createActionRequestBody(params)) StakingActionCommonType.ENTER -> stakeKitApi.createEnterAction(
StakingActionCommonType.EXIT -> stakeKitApi.createExitAction(createActionRequestBody(params)) createActionRequestBody(
userWalletId,
network,
params,
),
)
StakingActionCommonType.EXIT -> stakeKitApi.createExitAction(
createActionRequestBody(
userWalletId,
network,
params,
),
)
StakingActionCommonType.PENDING -> stakeKitApi.createPendingAction( StakingActionCommonType.PENDING -> stakeKitApi.createPendingAction(
createPendingActionRequestBody(params), 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) { return withContext(dispatchers.io) {
val gasEstimateDTO = when (params.actionCommonType) { val gasEstimateDTO = when (params.actionCommonType) {
StakingActionCommonType.ENTER -> stakeKitApi.estimateGasOnEnter(createActionRequestBody(params)) StakingActionCommonType.ENTER -> stakeKitApi.estimateGasOnEnter(
StakingActionCommonType.EXIT -> stakeKitApi.estimateGasOnExit(createActionRequestBody(params)) createActionRequestBody(
userWalletId,
network,
params,
),
)
StakingActionCommonType.EXIT -> stakeKitApi.estimateGasOnExit(
createActionRequestBody(
userWalletId,
network,
params,
),
)
StakingActionCommonType.PENDING -> stakeKitApi.estimateGasOnPending( StakingActionCommonType.PENDING -> stakeKitApi.estimateGasOnPending(
createPendingActionRequestBody(params), 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( return ActionRequestBody(
integrationId = params.integrationId, integrationId = params.integrationId,
addresses = Address(params.address), addresses = Address(
address = params.address,
additionalAddresses = createAdditionalAddresses(userWalletId, network, params),
),
args = ActionRequestBodyArgs( args = ActionRequestBodyArgs(
amount = params.amount.toFormattedString(params.token.decimals), amount = params.amount.toPlainString(),
inputToken = tokenConverter.convertBack(params.token), inputToken = tokenConverter.convertBack(params.token),
validatorAddress = params.validatorAddress, validatorAddress = params.validatorAddress,
), ),
@ -453,12 +498,29 @@ internal class DefaultStakingRepository(
type = params.type ?: StakingActionType.UNKNOWN, type = params.type ?: StakingActionType.UNKNOWN,
passthrough = params.passthrough.orEmpty(), passthrough = params.passthrough.orEmpty(),
args = ActionRequestBodyArgs( args = ActionRequestBodyArgs(
amount = params.amount.toFormattedString(params.token.decimals), amount = params.amount.toPlainString(),
validatorAddress = params.validatorAddress, 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 { override fun isStakeMoreAvailable(networkId: Network.ID): Boolean {
val blockchain = Blockchain.fromId(networkId.value) val blockchain = Blockchain.fromId(networkId.value)
return when (blockchain) { 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.datasource.local.token.StakingYieldsStore
import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module import dagger.Module
@ -35,6 +36,7 @@ internal object StakingDataModule {
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
stakingFeatureToggle: StakingFeatureToggles, stakingFeatureToggle: StakingFeatureToggles,
cacheRegistry: CacheRegistry, cacheRegistry: CacheRegistry,
walletManagersFacade: WalletManagersFacade,
): StakingRepository { ): StakingRepository {
return DefaultStakingRepository( return DefaultStakingRepository(
stakeKitApi = stakeKitApi, stakeKitApi = stakeKitApi,
@ -44,6 +46,7 @@ internal object StakingDataModule {
dispatchers = dispatchers, dispatchers = dispatchers,
cacheRegistry = cacheRegistry, cacheRegistry = cacheRegistry,
stakingFeatureToggle = stakingFeatureToggle, stakingFeatureToggle = stakingFeatureToggle,
walletManagersFacade = walletManagersFacade,
) )
} }

View file

@ -12,6 +12,7 @@ data class ActionParams(
val address: String, val address: String,
val validatorAddress: String, val validatorAddress: String,
val token: Token, val token: Token,
val publicKey: String? = null,
val passthrough: String? = null, val passthrough: String? = null,
val type: StakingActionType? = 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.model.stakekit.transaction.StakingGasEstimate
import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository 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. * Use case for staking gas estimation.
@ -15,9 +17,13 @@ class EstimateGasUseCase(
private val stakingErrorResolver: StakingErrorResolver, 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 { return Either.catch {
stakingRepository.estimateGas(params) stakingRepository.estimateGas(userWalletId, network, params)
}.mapLeft { }.mapLeft {
stakingErrorResolver.resolve(it) 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.model.stakekit.transaction.StakingTransaction
import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository 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 import kotlinx.coroutines.delay
/** /**
@ -16,9 +18,13 @@ class GetStakingTransactionUseCase(
private val stakingErrorResolver: StakingErrorResolver, 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 { 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 // workaround, sometimes transaction is not created immediately after actions/enter
delay(PATCH_TRANSACTION_REQUEST_DELAY) delay(PATCH_TRANSACTION_REQUEST_DELAY)

View file

@ -61,9 +61,9 @@ interface StakingRepository {
addresses: List<CryptoCurrencyAddress>, addresses: List<CryptoCurrencyAddress>,
): YieldBalanceList ): 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 suspend fun constructTransaction(transactionId: String): StakingTransaction

View file

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

View file

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

View file

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

View file

@ -12,16 +12,13 @@ internal class AmountMaxValueStateTransformer(
private val yield: Yield, private val yield: Yield,
) : Transformer<StakingUiState> { ) : Transformer<StakingUiState> {
private val amountRequirementStateTransformer by lazy(LazyThreadSafetyMode.NONE) { private val amountRequirementStateTransformer = AmountRequirementStateTransformer(
val value = cryptoCurrencyStatus.value.amount cryptoCurrencyStatus = cryptoCurrencyStatus,
yield = yield,
value = cryptoCurrencyStatus.value.amount
?.parseBigDecimal(cryptoCurrencyStatus.currency.decimals) ?.parseBigDecimal(cryptoCurrencyStatus.currency.decimals)
.orEmpty() .orEmpty(),
AmountRequirementStateTransformer( )
cryptoCurrencyStatus,
yield,
value,
)
}
override fun transform(prevState: StakingUiState): StakingUiState { override fun transform(prevState: StakingUiState): StakingUiState {
val updatedAmountState = AmountFieldMaxAmountTransformer(cryptoCurrencyStatus).transform(prevState.amountState) 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.extensions.wrappedList
import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.parseToBigDecimal 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.staking.model.stakekit.Yield
import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.R
@ -18,38 +19,47 @@ internal class AmountRequirementStateTransformer(
) : Transformer<AmountState> { ) : Transformer<AmountState> {
override fun transform(prevState: AmountState): AmountState { override fun transform(prevState: AmountState): AmountState {
val amountRequirements = yield.args.enter.args[Yield.Args.ArgType.AMOUNT] 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) { return if (prevState !is AmountState.Data || amountRequirements == null) {
prevState prevState
} else { } else {
val isAlreadyErrorState = prevState.amountTextField.isError updateWithError(prevState, amountRequirements)
val isAmountRequired = amountRequirements.required }
val isAmountZero = amountDecimal.isZero() }
val isExceedsRequirements =
amountRequirements.maximum?.compareTo(amountDecimal) == -1 ||
amountRequirements.minimum?.compareTo(amountDecimal) == 1
val isRequirementError = !isAmountZero && isAmountRequired && isExceedsRequirements && !isAlreadyErrorState private fun updateWithError(prevState: AmountState.Data, amountRequirements: AddressArgument): AmountState {
if (isRequirementError) { val isRequirementError = isRequirementError(prevState, amountRequirements)
prevState.copy( return if (isRequirementError) {
amountTextField = prevState.amountTextField.copy( prevState.copy(
isError = true, amountTextField = prevState.amountTextField.copy(
error = resourceReference( isError = true,
R.string.staking_amount_requirement_error, error = resourceReference(
wrappedList( R.string.staking_amount_requirement_error,
BigDecimalFormatter.formatCryptoAmount( wrappedList(
amountRequirements.minimum, BigDecimalFormatter.formatCryptoAmount(
cryptoCurrencyStatus.currency.symbol, amountRequirements.minimum,
cryptoCurrencyStatus.currency.decimals, 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 pendingActions = confirmationState.pendingActions
val stakingTransaction = getStakingTransactionUseCase( val stakingTransaction = getStakingTransactionUseCase(
userWalletId = userWalletId,
network = cryptoCurrencyStatus.currency.network,
params = ActionParams( params = ActionParams(
actionCommonType = getStakingCommonType(), actionCommonType = getStakingCommonType(),
integrationId = yield.id, integrationId = yield.id,
@ -158,6 +160,8 @@ internal class StakingViewModel @Inject constructor(
val cryptoCurrencyValue = cryptoCurrencyStatus.value val cryptoCurrencyValue = cryptoCurrencyStatus.value
val stakingGasEstimate = estimateGasUseCase( val stakingGasEstimate = estimateGasUseCase(
userWalletId = userWalletId,
network = cryptoCurrencyStatus.currency.network,
params = ActionParams( params = ActionParams(
actionCommonType = getStakingCommonType(), actionCommonType = getStakingCommonType(),
integrationId = yield.id, integrationId = yield.id,