Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-25 17:17:19 +05:00
parent 6d7ae39ba2
commit a09aae7e41
23 changed files with 337 additions and 87 deletions

View file

@ -7,6 +7,8 @@ import com.tangem.blockchain.common.Wallet
import com.tangem.common.CompletionResult
import com.tangem.domain.card.models.TwinKey
import com.tangem.domain.models.scan.isRing
import com.tangem.operations.sign.SignData
import com.tangem.tap.domain.tasks.MultiSignHashTask
import com.tangem.tap.domain.tasks.SignHashesTask
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
@ -64,6 +66,40 @@ class TangemSigner(
is CompletionResult.Failure -> CompletionResult.Failure(result.error)
}
}
override suspend fun multiSign(
dataToSign: List<SignData>,
publicKey: Wallet.PublicKey,
): CompletionResult<Map<ByteArray, ByteArray>> {
return suspendCancellableCoroutine { continuation ->
val task = MultiSignHashTask(dataToSign, publicKey, twinKey?.getPairKey(publicKey.seedKey))
tangemSdk.startSessionWithRunnable(
runnable = task,
cardId = cardId,
initialMessage = initialMessage,
) { result ->
when (result) {
is CompletionResult.Success -> {
signerCallback(
TangemSignerResponse(
totalSignedHashes = result.data.totalSignedHashes,
remainingSignatures = result.data.remainingSignatures,
isRing = result.data.batchId?.let(::isRing) ?: false,
),
)
if (continuation.isActive) {
continuation.resume(CompletionResult.Success(result.data.signatures))
}
}
is CompletionResult.Failure ->
if (continuation.isActive) {
continuation.resume(CompletionResult.Failure(result.error))
}
}
}
}
}
}
data class TangemSignerResponse(

View file

@ -0,0 +1,80 @@
package com.tangem.tap.domain.tasks
import com.tangem.blockchain.common.Wallet
import com.tangem.common.CompletionResult
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.common.core.TangemSdkError
import com.tangem.operations.CommandResponse
import com.tangem.operations.sign.MultipleSignCommand
import com.tangem.operations.sign.SignData
class TangemMultiSignHashResponse(
val signatures: Map<ByteArray, ByteArray>,
val totalSignedHashes: Int?,
val remainingSignatures: Int?,
val batchId: String?,
) : CommandResponse
class MultiSignHashTask(
private val dataToSign: List<SignData>,
private val publicKey: Wallet.PublicKey,
private val pairWalletPublicKey: ByteArray?,
) : CardSessionRunnable<TangemMultiSignHashResponse> {
override fun run(session: CardSession, callback: CompletionCallback<TangemMultiSignHashResponse>) {
MultipleSignCommand(dataToSign, publicKey.seedKey).run(session) { response ->
when (response) {
is CompletionResult.Success -> {
val card = session.environment.card
callback(
CompletionResult.Success(
TangemMultiSignHashResponse(
signatures = response.data.associate { it.walletPublicKey to it.signature },
totalSignedHashes = response.data.last().totalSignedHashes,
remainingSignatures = card?.wallet(publicKey.seedKey)?.remainingSignatures,
batchId = card?.batchId,
),
),
)
}
is CompletionResult.Failure -> {
when {
response.error is TangemSdkError.WalletNotFound && pairWalletPublicKey != null -> {
sign(session, pairWalletPublicKey, callback)
}
else -> callback(CompletionResult.Failure(response.error))
}
}
}
}
}
private fun sign(
session: CardSession,
publicKey: ByteArray,
callback: CompletionCallback<TangemMultiSignHashResponse>,
) {
MultipleSignCommand(dataToSign, publicKey).run(session) { response ->
when (response) {
is CompletionResult.Success -> {
val card = session.environment.card
callback(
CompletionResult.Success(
TangemMultiSignHashResponse(
signatures = response.data.associate { it.walletPublicKey to it.signature },
totalSignedHashes = response.data.last().totalSignedHashes,
remainingSignatures = card?.wallet(publicKey)?.remainingSignatures,
batchId = card?.batchId,
),
),
)
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(response.error))
}
}
}
}
}

View file

@ -27,6 +27,7 @@ dependencies {
/** Project - Common */
implementation(projects.core.ui)
implementation(projects.core.utils)
implementation(projects.libs.crypto)
/** Project - Domain */
implementation(projects.domain.appCurrency.models)

View file

@ -364,6 +364,7 @@ object NotificationsFactory {
)
}
}
else -> return
}
}

View file

@ -15,6 +15,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.StatusSource
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.isZero
@ -199,7 +200,11 @@ class TokenItemStateConverter(
is CryptoCurrencyStatus.NoAccount,
-> {
TokenItemState.Subtitle2State.TextContent(
text = status.getFormattedCryptoAmount(includeStaking = true),
text = status.getFormattedCryptoAmount(
includeStaking = BlockchainUtils.isIncludeStakingTotalBalance(
status.currency.network.id.value,
),
),
isFlickering = status.value.isFlickering(),
)
}
@ -222,7 +227,12 @@ class TokenItemStateConverter(
is CryptoCurrencyStatus.NoAccount,
-> {
TokenItemState.FiatAmountState.Content(
text = status.getFormattedFiatAmount(appCurrency = appCurrency, includeStaking = true),
text = status.getFormattedFiatAmount(
appCurrency = appCurrency,
includeStaking = BlockchainUtils.isIncludeStakingTotalBalance(
status.currency.network.id.value,
),
),
isFlickering = status.value.isFlickering(),
icons = buildList {
if (!status.getStakedBalance().isZero()) {

View file

@ -34,6 +34,7 @@ import com.tangem.datasource.local.token.StakingBalanceStore
import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.datasource.local.token.converter.StakingNetworkTypeConverter
import com.tangem.datasource.local.token.converter.TokenConverter
import com.tangem.domain.common.TapWorkarounds.isWallet2
import com.tangem.domain.staking.model.StakingApproval
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
@ -55,6 +56,7 @@ 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.usecase.GetUserWalletUseCase
import com.tangem.lib.crypto.BlockchainUtils.isCardano
import com.tangem.lib.crypto.BlockchainUtils.isSolana
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.orZero
@ -219,14 +221,11 @@ internal class DefaultStakingRepository(
val userWallet = getUserWalletUseCase(userWalletId).getOrElse {
error("Failed to get user wallet")
}
val blockchainId = cryptoCurrency.network.id.value
return when {
isSolana(cryptoCurrency.network.id.value) -> {
INVALID_BATCHES_FOR_SOLANA.contains(userWallet.scanResponse.card.batchId)
}
else -> {
false
}
isSolana(blockchainId) -> INVALID_BATCHES_FOR_SOLANA.contains(userWallet.scanResponse.card.batchId)
isCardano(blockchainId) -> !userWallet.scanResponse.card.isWallet2
else -> false
}
}

View file

@ -39,4 +39,12 @@ enum class StakingActionType {
MIGRATE -> "Migrate"
UNKNOWN -> "Unknown"
}
val isRestake
get() = when (this) {
RESTAKE,
STAKE,
-> true
else -> false
}
}

View file

@ -7,6 +7,7 @@ import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.lib.crypto.BlockchainUtils.isIncludeStakingTotalBalance
import com.tangem.utils.extensions.orZero
import java.math.BigDecimal
@ -21,6 +22,8 @@ internal class TokenListFiatBalanceOperations(
if (isAnyTokenLoading) return fiatBalance
for (token in currencies) {
val networkId = token.currency.network.id.value
val includeStakingBalance = isIncludeStakingTotalBalance(networkId)
when (val status = token.value) {
is CryptoCurrencyStatus.Loading -> {
fiatBalance = TotalFiatBalance.Loading
@ -35,7 +38,7 @@ internal class TokenListFiatBalanceOperations(
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
-> {
if (BlockchainUtils.isIncludeToBalanceOnError(token.currency.network.id.value)) {
if (BlockchainUtils.isIncludeToBalanceOnError(networkId)) {
fiatBalance = recalculateNoAccountBalance(status, fiatBalance)
} else {
fiatBalance = TotalFiatBalance.Failed
@ -46,10 +49,10 @@ internal class TokenListFiatBalanceOperations(
fiatBalance = recalculateNoAccountBalance(status, fiatBalance)
}
is CryptoCurrencyStatus.Loaded -> {
fiatBalance = recalculateBalance(status, fiatBalance)
fiatBalance = recalculateBalance(status, fiatBalance, includeStakingBalance)
}
is CryptoCurrencyStatus.Custom -> {
fiatBalance = recalculateBalance(status, fiatBalance)
fiatBalance = recalculateBalance(status, fiatBalance, includeStakingBalance)
}
}
}
@ -74,12 +77,16 @@ internal class TokenListFiatBalanceOperations(
private fun recalculateBalance(
status: CryptoCurrencyStatus.Loaded,
currentBalance: TotalFiatBalance,
includeStakingBalance: Boolean,
): TotalFiatBalance {
return with(currentBalance) {
val yieldBalance = status.yieldBalance as? YieldBalance.Data
val stakingBalance = yieldBalance?.getTotalWithRewardsStakingBalance().orZero()
val fiatStakingBalance = status.fiatRate.times(stakingBalance)
val fiatStakingBalance = if (includeStakingBalance) {
status.fiatRate.times(stakingBalance)
} else {
BigDecimal.ZERO
}
(this as? TotalFiatBalance.Loaded)?.copy(
amount = this.amount + status.fiatAmount + fiatStakingBalance,
) ?: TotalFiatBalance.Loaded(
@ -93,11 +100,16 @@ internal class TokenListFiatBalanceOperations(
private fun recalculateBalance(
status: CryptoCurrencyStatus.Custom,
currentBalance: TotalFiatBalance,
includeStakingBalance: Boolean,
): TotalFiatBalance {
return with(currentBalance) {
val isTokenAmountCanBeSummarized = status.fiatAmount != null
val yieldBalance = (status.yieldBalance as? YieldBalance.Data)?.getTotalWithRewardsStakingBalance().orZero()
val fiatYieldBalance = status.fiatRate?.times(yieldBalance).orZero()
val fiatYieldBalance = if (includeStakingBalance) {
status.fiatRate?.times(yieldBalance).orZero()
} else {
BigDecimal.ZERO
}
(this as? TotalFiatBalance.Loaded)?.copy(
amount = this.amount + status.fiatAmount.orZero() + fiatYieldBalance,
isAllAmountsSummarized = isTokenAmountCanBeSummarized,

View file

@ -9,6 +9,7 @@ import arrow.core.raise.ensureNotNull
import arrow.core.toNonEmptyListOrNull
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.tokens.model.*
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.extensions.orZero
import java.math.BigDecimal
@ -99,7 +100,11 @@ internal class TokenListSortingOperations(
private fun CryptoCurrencyStatus.getTotalBalance(): BigDecimal {
val yieldBalance = value.yieldBalance as? YieldBalance.Data
val totalYieldBalance = yieldBalance?.getTotalWithRewardsStakingBalance().orZero()
val totalFiatYieldBalance = totalYieldBalance.multiply(value.fiatRate.orZero())
val totalFiatYieldBalance = if (!BlockchainUtils.isIncludeStakingTotalBalance(currency.network.id.value)) {
totalYieldBalance.multiply(value.fiatRate.orZero())
} else {
BigDecimal.ZERO
}
return value.fiatAmount?.plus(totalFiatYieldBalance).orZero()
}

View file

@ -36,7 +36,6 @@ import com.tangem.domain.staking.model.StakingApproval
import com.tangem.domain.staking.model.stakekit.*
import com.tangem.domain.staking.model.stakekit.action.StakingAction
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.model.CryptoCurrency
@ -45,8 +44,6 @@ import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase
import com.tangem.domain.transaction.usecase.GetAllowanceUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase
import com.tangem.domain.utils.convertToSdkAmount
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
@ -72,6 +69,7 @@ import com.tangem.features.staking.impl.presentation.state.transformers.validato
import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount
import com.tangem.features.staking.impl.presentation.state.utils.isSingleAction
import com.tangem.features.staking.impl.presentation.state.utils.withStubUnstakeAction
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.*
import com.tangem.utils.extensions.isSingleItem
@ -108,7 +106,6 @@ internal class StakingModel @Inject constructor(
private val getCardInfoUseCase: GetCardInfoUseCase,
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase,
private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
private val validateTransactionUseCase: ValidateTransactionUseCase,
private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase,
private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase,
private val isAnyTokenStakedUseCase: IsAnyTokenStakedUseCase,
@ -123,7 +120,7 @@ internal class StakingModel @Inject constructor(
private val shareManager: ShareManager,
@DelayedWork private val coroutineScope: CoroutineScope,
private val innerRouter: InnerStakingRouter,
private val appRouter: AppRouter,
appRouter: AppRouter,
) : Model(), StakingClickIntents {
val uiState: StateFlow<StakingUiState> = stateController.uiState
@ -239,7 +236,7 @@ internal class StakingModel @Inject constructor(
return
}
isInitialInfoStep && noBalanceState -> {
stateController.update(
val list = buildList {
SetConfirmationStateInitTransformer(
isEnter = true,
isExplicitExit = false,
@ -248,8 +245,27 @@ internal class StakingModel @Inject constructor(
stakingApproval = stakingApproval,
stakingAllowance = stakingAllowance,
yieldArgs = yield.args,
),
)
).let(::add)
if (BlockchainUtils.isSkipAmountEnter(uiState.value.cryptoCurrencyBlockchainId)) {
ValidatorSelectChangeTransformer(
selectedValidator = yield.preferredValidators.firstOrNull(),
yield = yield,
).let(::add)
SetAmountDataTransformer(
clickIntents = this@StakingModel,
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
userWalletProvider = Provider { userWallet },
appCurrencyProvider = Provider { appCurrency },
).let(::add)
AmountMaxValueStateTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
minimumTransactionAmount = minimumTransactionAmount,
actionType = uiState.value.actionType,
yield = yield,
).let(::add)
}
}
stateController.updateAll(*list.toTypedArray())
}
}
stakingStateRouter.onNextClick()
@ -633,16 +649,6 @@ internal class StakingModel @Inject constructor(
} else {
null
}
val validation = amount?.let {
validateTransactionUseCase(
userWalletId = userWalletId,
amount = amount.convertToSdkAmount(cryptoCurrencyStatus.currency),
fee = feeState?.fee,
memo = null,
destination = "",
network = cryptoCurrencyStatus.currency.network,
).leftOrNull()
}
val balanceAfterTransaction = calculateBalanceAfterTransaction(
amount = amount.orZero(),
@ -663,7 +669,6 @@ internal class StakingModel @Inject constructor(
appCurrencyProvider = Provider { appCurrency },
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
currencyWarning = currencyWarning,
validatorError = validation,
currencyCheck = currencyStatus,
isSubtractAvailable = isAmountSubtractAvailable,
feeError = feeError,
@ -1021,7 +1026,7 @@ internal class StakingModel @Inject constructor(
}
private fun isExplicitExit(balanceType: BalanceType, pendingAction: PendingAction?): Boolean {
return balanceType == BalanceType.STAKED && pendingAction?.type != StakingActionType.RESTAKE
return balanceType == BalanceType.STAKED && pendingAction?.type?.isRestake == false
}
private fun isAssentState(): Boolean {

View file

@ -29,6 +29,14 @@ internal object StakingNotification {
title = resourceReference(R.string.common_error),
subtitle = subtitle,
)
data class CardanoMinimumBalance(
val title: TextReference,
val subtitle: TextReference,
) : StakingNotification.Error(
title = title,
subtitle = subtitle,
)
}
sealed class Warning(

View file

@ -11,6 +11,7 @@ import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.features.staking.impl.presentation.state.utils.isCompositePendingActions
import com.tangem.features.staking.impl.presentation.state.utils.isTronStakedBalance
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.lib.crypto.BlockchainUtils.isSkipAmountEnter
import com.tangem.utils.extensions.isPositive
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.ImmutableList
@ -45,12 +46,12 @@ internal class SetConfirmationStateInitTransformer(
override fun transform(prevState: StakingUiState): StakingUiState {
val actionType = when {
isEnter -> StakingActionCommonType.Enter(
skipEnterAmount = isAmountEnterSkipped(prevState),
skipEnterAmount = isSkipAmountEnter(prevState.cryptoCurrencyBlockchainId),
)
isImplicitExit || isExplicitExit -> StakingActionCommonType.Exit(isPartiallyUnstakeDisabled(prevState))
else -> when (pendingAction?.type) {
StakingActionType.STAKE -> StakingActionCommonType.Enter(
skipEnterAmount = isAmountEnterSkipped(prevState),
skipEnterAmount = isSkipAmountEnter(prevState.cryptoCurrencyBlockchainId),
)
StakingActionType.UNSTAKE -> StakingActionCommonType.Exit(
partiallyUnstakeDisabled = isPartiallyUnstakeDisabled(prevState),
@ -100,8 +101,4 @@ internal class SetConfirmationStateInitTransformer(
val max = exitAmount.maximum ?: return false
return !min.isPositive() && !max.isPositive()
}
private fun isAmountEnterSkipped(state: StakingUiState): Boolean {
return BlockchainUtils.isCardano(state.cryptoCurrencyBlockchainId)
}
}

View file

@ -1,30 +1,33 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.remove
import com.tangem.common.ui.amountScreen.converters.AmountStateConverter
import com.tangem.common.ui.amountScreen.models.AmountParameters
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.list.RoundedListWithDividersItemData
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.stakekit.BalanceItem
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingStep
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
import com.tangem.features.staking.impl.presentation.state.converters.RewardsValidatorStateConverter
import com.tangem.features.staking.impl.presentation.state.converters.YieldBalancesConverter
import com.tangem.features.staking.impl.presentation.state.utils.getRewardScheduleText
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
import com.tangem.lib.crypto.BlockchainUtils.isPolkadot
import com.tangem.utils.Provider
import com.tangem.utils.isNullOrZero
import com.tangem.utils.transformer.Transformer
@ -145,7 +148,8 @@ internal class SetInitialDataStateTransformer(
cryptoCurrencyStatus: CryptoCurrencyStatus,
): RoundedListWithDividersItemData? {
val minimumCryptoAmount = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum ?: return null
if (!isPolkadot(cryptoCurrencyStatus.currency.network.id.value)) return null
val blockchainId = cryptoCurrencyStatus.currency.network.id.value
if (!showMinimumRequirementInfo(blockchainId)) return null
val formattedAmount = minimumCryptoAmount.format { crypto(cryptoCurrencyStatus.currency) }
@ -239,6 +243,10 @@ internal class SetInitialDataStateTransformer(
return resourceReference(R.string.common_range, wrappedList(formattedMinApr, formattedMaxApr))
}
private fun showMinimumRequirementInfo(blockchainId: String): Boolean {
return blockchainId == Blockchain.Polkadot.id || blockchainId == Blockchain.Cardano.id
}
private companion object {
val EQUALITY_THRESHOLD = BigDecimal(1E-10)

View file

@ -1,6 +1,5 @@
package com.tangem.features.staking.impl.presentation.state.transformers.notifications
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.common.ui.notifications.NotificationsFactory.addDustWarningNotification
@ -11,7 +10,6 @@ import com.tangem.common.ui.notifications.NotificationsFactory.addFeeUnreachable
import com.tangem.common.ui.notifications.NotificationsFactory.addRentExemptionNotification
import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountErrorNotification
import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification
import com.tangem.common.ui.notifications.NotificationsFactory.addValidateTransactionNotifications
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.stakekit.Yield
@ -40,7 +38,6 @@ internal class AddStakingNotificationsTransformer(
private val appCurrencyProvider: Provider<AppCurrency>,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
private val currencyWarning: CryptoCurrencyWarning?,
private val validatorError: Throwable?,
private val feeError: GetFeeError?,
private val currencyCheck: CryptoCurrencyCheck,
private val isSubtractAvailable: Boolean,
@ -81,7 +78,7 @@ internal class AddStakingNotificationsTransformer(
amountValue = amountValue,
feeValue = feeValue,
reduceAmountBy = reduceAmountBy,
).max(minimumRequirement)
)
} else {
// No amount is taken from account balance on exit or pending actions
BigDecimal.ZERO
@ -212,15 +209,6 @@ internal class AddStakingNotificationsTransformer(
appCurrency = appCurrency,
cryptoCurrencyStatus = cryptoCurrencyStatus,
)
// blockchain specific
addValidateTransactionNotifications(
dustValue = currencyCheck.dustValue.orZero(),
minAdaValue = (feeState?.fee as? Fee.CardanoToken)?.minAdaValue,
validationError = validatorError,
cryptoCurrency = cryptoCurrency,
onReduceClick = prevState.clickIntents::onAmountReduceToClick,
)
}
private fun MutableList<NotificationUM>.addStakeExceedBalanceNotification(

View file

@ -14,6 +14,7 @@ import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.StakingNotification
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.lib.crypto.BlockchainUtils.isCardano
import com.tangem.lib.crypto.BlockchainUtils.isCosmos
import com.tangem.lib.crypto.BlockchainUtils.isTron
import com.tangem.utils.Provider
@ -46,7 +47,10 @@ internal class StakingInfoNotificationsFactory(
when (prevState.actionType) {
is StakingActionCommonType.Enter -> addEnterInfoNotifications(sendingAmount, feeValue)
is StakingActionCommonType.Exit -> addExitInfoNotifications()
is StakingActionCommonType.Pending -> addPendingInfoNotifications(prevState)
is StakingActionCommonType.Pending -> {
addCardanoRestakeMinimumAmountNotification(feeValue)
addPendingInfoNotifications(prevState)
}
}
}
@ -70,7 +74,9 @@ internal class StakingInfoNotificationsFactory(
sendingAmount: BigDecimal,
feeValue: BigDecimal,
) {
addCardanoStakeMinimumAmountNotification(feeValue)
addTronRevoteNotification()
addCardanoStakeNotification()
addStakingEntireBalanceNotification(sendingAmount, feeValue)
}
@ -149,6 +155,48 @@ internal class StakingInfoNotificationsFactory(
}
}
private fun MutableList<NotificationUM>.addCardanoStakeNotification() {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val isCardano = isCardano(cryptoCurrencyStatus.currency.network.id.value)
if (isCardano) {
add(
StakingNotification.Info.Ordinary(
title = resourceReference(R.string.staking_notification_additional_ada_deposit_title),
text = resourceReference(R.string.staking_notification_additional_ada_deposit_text),
),
)
}
}
private fun MutableList<NotificationUM>.addCardanoStakeMinimumAmountNotification(feeValue: BigDecimal) {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val isCardano = isCardano(cryptoCurrencyStatus.currency.network.id.value)
val balance = cryptoCurrencyStatus.value.amount.orZero()
if (isCardano && balance - feeValue < MINIMUM_STAKE_BALANCE) {
add(
StakingNotification.Error.CardanoMinimumBalance(
title = resourceReference(R.string.staking_notification_minimum_balance_title),
subtitle = resourceReference(R.string.staking_notification_minimum_stake_ada_text),
),
)
}
}
private fun MutableList<NotificationUM>.addCardanoRestakeMinimumAmountNotification(feeValue: BigDecimal) {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val isCardano = isCardano(cryptoCurrencyStatus.currency.network.id.value)
val balance = cryptoCurrencyStatus.value.amount.orZero()
if (isCardano && balance - feeValue < MINIMUM_RESTAKE_BALANCE) {
add(
StakingNotification.Error.CardanoMinimumBalance(
title = resourceReference(R.string.staking_notification_minimum_restake_ada_title),
subtitle = resourceReference(R.string.staking_notification_minimum_restake_ada_text),
),
)
}
}
private fun MutableList<NotificationUM>.addStakingEntireBalanceNotification(
sendingAmount: BigDecimal,
feeValue: BigDecimal,
@ -158,7 +206,7 @@ internal class StakingInfoNotificationsFactory(
val isEntireBalance = sendingAmount.plus(feeValue) == balance
if (isEntireBalance && isSubtractAvailable) {
if (isEntireBalance && isSubtractAvailable && !isCardano(cryptoCurrencyStatus.currency.network.id.value)) {
add(StakingNotification.Info.StakeEntireBalance)
}
}
@ -179,4 +227,9 @@ internal class StakingInfoNotificationsFactory(
add(StakingNotification.Warning.LowStakedBalance)
}
}
private companion object {
val MINIMUM_STAKE_BALANCE = "5".toBigDecimal()
val MINIMUM_RESTAKE_BALANCE = "3".toBigDecimal()
}
}

View file

@ -7,6 +7,7 @@ import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.lib.crypto.BlockchainUtils.isBSC
import com.tangem.lib.crypto.BlockchainUtils.isCardano
import com.tangem.lib.crypto.BlockchainUtils.isSolana
import com.tangem.lib.crypto.BlockchainUtils.isTron
import kotlinx.collections.immutable.ImmutableList
@ -37,14 +38,12 @@ internal fun StakingActionType?.getPendingActionTitle(): TextReference = when (t
internal fun isSingleAction(networkId: String, activeStake: BalanceState): Boolean {
val isSingleAction = activeStake.pendingActions.size <= 1 // Either single or none pending actions
val isCompositePendingActions = isCompositePendingActions(networkId, activeStake.pendingActions)
val isBscRestake = isBSC(networkId) && activeStake.pendingActions.any {
it.type == StakingActionType.RESTAKE
}
val isRestake = activeStake.pendingActions.any { it.type.isRestake }
return isSingleAction && !isBscRestake || isCompositePendingActions
return isSingleAction && !isRestake || isCompositePendingActions
}
internal fun withStubUnstakeAction(networkId: String, activeStake: BalanceState) = if (isBSC(networkId)) {
internal fun withStubUnstakeAction(networkId: String, activeStake: BalanceState) = if (isStubUnstakeAction(networkId)) {
activeStake.pendingActions.plus(
PendingAction(
type = StakingActionType.UNSTAKE,
@ -65,4 +64,8 @@ internal fun isCompositePendingActions(networkId: String, pendingActions: Immuta
isSolana(networkId) -> pendingActions?.any { it.type == StakingActionType.WITHDRAW } == true
else -> false
}
}
private fun isStubUnstakeAction(networkId: String): Boolean {
return isBSC(networkId) || isCardano(networkId)
}

View file

@ -5,6 +5,7 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@ -72,6 +73,7 @@ internal fun StakingConfirmationContent(
)
StakingFeeBlock(feeState = state.feeState, isTransactionSent = isTransactionSent)
NotificationsBlock(notifications = state.notifications)
Spacer(Modifier)
}
}

View file

@ -1,13 +1,14 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
@ -26,7 +27,8 @@ internal class TokenDetailsBalanceSelectStateConverter(
val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data
val stakingCryptoAmount = yieldBalance?.getTotalWithRewardsStakingBalance()
val stakingFiatAmount = stakingCryptoAmount?.let { cryptoCurrencyStatus.value.fiatRate?.multiply(it) }
val includeStakingTotalBalance =
BlockchainUtils.isIncludeStakingTotalBalance(cryptoCurrencyStatus.currency.network.id.value)
copy(
tokenBalanceBlockState = if (tokenBalanceBlockState is TokenDetailsBalanceBlockState.Content) {
tokenBalanceBlockState.copy(
@ -36,11 +38,13 @@ internal class TokenDetailsBalanceSelectStateConverter(
stakingFiatAmount = stakingFiatAmount,
selectedBalanceType = value.type,
appCurrency = appCurrencyProvider(),
includeStaking = includeStakingTotalBalance,
),
displayCryptoBalance = formatCryptoAmount(
status = cryptoCurrencyStatus,
stakingCryptoAmount = stakingCryptoAmount,
selectedBalanceType = value.type,
includeStaking = includeStakingTotalBalance,
),
)
} else {
@ -55,25 +59,26 @@ internal class TokenDetailsBalanceSelectStateConverter(
stakingFiatAmount: BigDecimal?,
selectedBalanceType: BalanceType,
appCurrency: AppCurrency,
includeStaking: Boolean,
): String {
val fiatAmount = status.fiatAmount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN
val totalAmount = fiatAmount.getBalance(selectedBalanceType, stakingFiatAmount)
val fiatAmount = status.fiatAmount?.getBalance(selectedBalanceType, stakingFiatAmount, includeStaking)
return BigDecimalFormatter.formatFiatAmount(
fiatAmount = totalAmount,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
return fiatAmount.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
}
}
private fun formatCryptoAmount(
status: CryptoCurrencyStatus,
stakingCryptoAmount: BigDecimal?,
selectedBalanceType: BalanceType,
includeStaking: Boolean,
): String {
val amount = status.value.amount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN
val totalAmount = amount.getBalance(selectedBalanceType, stakingCryptoAmount)
val amount = status.value.amount?.getBalance(selectedBalanceType, stakingCryptoAmount, includeStaking)
return totalAmount.format { crypto(status.currency) }
return amount.format { crypto(status.currency) }
}
}

View file

@ -19,13 +19,14 @@ import com.tangem.domain.staking.model.stakekit.RewardBlockType
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsTxHistoryTransactionStateConverter
import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
import com.tangem.features.tokendetails.impl.R
import com.tangem.lib.crypto.BlockchainUtils.isBSC
import com.tangem.lib.crypto.BlockchainUtils.isIncludeStakingTotalBalance
import com.tangem.lib.crypto.BlockchainUtils.isSolana
import com.tangem.utils.Provider
import com.tangem.utils.StringsSigns.DASH_SIGN
@ -98,6 +99,7 @@ internal class TokenDetailsLoadedBalanceConverter(
val stakingCryptoAmount = (status.value.yieldBalance as? YieldBalance.Data)?.getTotalWithRewardsStakingBalance()
val stakingFiatAmount = stakingCryptoAmount?.let { status.value.fiatRate?.multiply(it) }
val isBalanceSelectorEnabled = !stakingCryptoAmount.isNullOrZero()
val includeStakingTotalBalance = isIncludeStakingTotalBalance(status.currency.network.id.value)
return when (status.value) {
is CryptoCurrencyStatus.NoQuote,
is CryptoCurrencyStatus.Loaded,
@ -112,11 +114,13 @@ internal class TokenDetailsLoadedBalanceConverter(
stakingFiatAmount,
currentState.selectedBalanceType,
appCurrencyProvider(),
includeStakingTotalBalance,
),
displayCryptoBalance = formatCryptoAmount(
status,
stakingCryptoAmount,
currentState.selectedBalanceType,
includeStakingTotalBalance,
),
balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig,
onBalanceSelect = clickIntents::onBalanceSelect,
@ -298,9 +302,10 @@ internal class TokenDetailsLoadedBalanceConverter(
stakingFiatAmount: BigDecimal?,
selectedBalanceType: BalanceType,
appCurrency: AppCurrency,
includeStaking: Boolean,
): String {
val fiatAmount = status.fiatAmount ?: return DASH_SIGN
val totalAmount = fiatAmount.getBalance(selectedBalanceType, stakingFiatAmount)
val totalAmount = fiatAmount.getBalance(selectedBalanceType, stakingFiatAmount, includeStaking)
return totalAmount.format {
fiat(
@ -314,9 +319,10 @@ internal class TokenDetailsLoadedBalanceConverter(
status: CryptoCurrencyStatus,
stakingCryptoAmount: BigDecimal?,
selectedBalanceType: BalanceType,
includeStaking: Boolean,
): String {
val amount = status.value.amount ?: return DASH_SIGN
val totalAmount = amount.getBalance(selectedBalanceType, stakingCryptoAmount)
val totalAmount = amount.getBalance(selectedBalanceType, stakingCryptoAmount, includeStaking)
return totalAmount.format { crypto(status.currency) }
}

View file

@ -3,8 +3,12 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.utils
import com.tangem.feature.tokendetails.presentation.tokendetails.state.BalanceType
import java.math.BigDecimal
internal fun BigDecimal.getBalance(selectedBalanceType: BalanceType, stakingAmount: BigDecimal?): BigDecimal {
return if (selectedBalanceType == BalanceType.ALL && stakingAmount != null) {
internal fun BigDecimal.getBalance(
selectedBalanceType: BalanceType,
stakingAmount: BigDecimal?,
includeStaking: Boolean,
): BigDecimal {
return if (selectedBalanceType == BalanceType.ALL && stakingAmount != null && includeStaking) {
this.plus(stakingAmount)
} else {
this

View file

@ -12,8 +12,10 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getTokenItemId
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.orZero
import java.math.BigDecimal
internal class CryptoCurrencyToDraggableItemConverter(
@ -63,7 +65,11 @@ internal class CryptoCurrencyToDraggableItemConverter(
private fun getFormattedFiatAmount(currency: CryptoCurrencyStatus, appCurrency: AppCurrency): String {
val yieldBalance = currency.value.yieldBalance as? YieldBalance.Data
val fiatRate = currency.value.fiatRate ?: BigDecimal.ZERO
val fiatYieldBalance = yieldBalance?.getTotalWithRewardsStakingBalance()?.multiply(fiatRate) ?: BigDecimal.ZERO
val fiatYieldBalance = if (BlockchainUtils.isIncludeStakingTotalBalance(currency.currency.network.id.value)) {
yieldBalance?.getTotalWithRewardsStakingBalance()?.multiply(fiatRate).orZero()
} else {
BigDecimal.ZERO
}
val fiatAmount = currency.value.fiatAmount ?: return BigDecimalFormatConstants.EMPTY_BALANCE_SIGN
return (fiatAmount + fiatYieldBalance).format { fiat(appCurrency.code, appCurrency.symbol) }

View file

@ -1,7 +1,7 @@
[versions]
tangemBlockchainSdk = "develop-965"
tangemBlockchainSdk = "develop-967"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-434"
tangemCardSdk = "develop-437"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
tangemVico = "2.0.0-alpha.25-tangem-developments8"
#tangemVico = "0.0.1" # Keep it! - used for local builds ^

View file

@ -18,6 +18,7 @@ import java.math.BigDecimal
* Temporary solution for domain specific logic for Blockchain.
* Instead of creating repositories and unnecessary and overkill use cases
*/
@Suppress("TooManyFunctions")
object BlockchainUtils {
private const val XRP_X_ADDRESS = 'X'
@ -137,6 +138,18 @@ object BlockchainUtils {
}
}
fun isIncludeStakingTotalBalance(blockchainId: String): Boolean {
val blockchain = Blockchain.fromId(blockchainId)
return blockchain != Blockchain.Cardano
}
fun isSkipAmountEnter(blockchainId: String): Boolean {
val blockchain = Blockchain.fromId(blockchainId)
return blockchain == Blockchain.Cardano
}
private fun getNetworkStandardName(blockchain: Blockchain): String {
return when (blockchain) {
Blockchain.Ethereum, Blockchain.EthereumTestnet -> "ERC20"