Updated on 2026-08-14
This commit is contained in:
parent
ff2907e26e
commit
957bb0dd56
13 changed files with 159 additions and 61 deletions
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue