Updated on 2026-08-14

This commit is contained in:
Tangem 2024-10-04 16:24:21 +03:00
commit 4b70b376ed
16 changed files with 297 additions and 64 deletions

View file

@ -111,6 +111,14 @@ class TransactionManagerImpl(
gasPremium = normalFee.gasPremium,
)
}
is Fee.Sui -> {
ProxyFee.Sui(
gasLimit = BigInteger.ZERO,
fee = convertToProxyAmount(amount = normalFee.amount),
gasPrice = normalFee.gasPrice,
gasBudget = normalFee.gasBudget,
)
}
else -> {
ProxyFee.Common(
gasLimit = BigInteger.ZERO,

View file

@ -490,7 +490,7 @@ object BigDecimalFormatter {
): String {
if (threeDigitsMethod) {
val scaledAmount = amount.setScale(scale, RoundingMode.HALF_UP)
val digitsCount = scaledAmount.longValueExact().toString().count()
val digitsCount = scaledAmount.toString().count()
val digitsToFormat = 6 - when (digitsCount % 3) {
0 -> 0
1 -> 2
@ -508,7 +508,7 @@ object BigDecimalFormatter {
return formatter.format(amount.setScale(scale, RoundingMode.HALF_UP))
} else {
val scaledAmount = amount.setScale(scale, RoundingMode.HALF_UP)
val digitsCount = scaledAmount.longValueExact().toString().count()
val digitsCount = scaledAmount.toString().count()
val digitsToFormat = 5 - when (digitsCount % 3) {
0 -> 0
1 -> 2

View file

@ -29,8 +29,9 @@ class YieldConverter(
validators = value.validators
.asSequence()
.filter { it.status == ValidatorStatusDTO.ACTIVE }
.sortedByDescending { it.apr }
.map { convertValidator(it) }
.sortedByDescending { it.isStrategicPartner }
.sortedByDescending { it.apr }
.toImmutableList(),
isAvailable = value.isAvailable,
)

View file

@ -8,6 +8,7 @@ import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository
import com.tangem.domain.wallets.models.UserWalletId
import org.joda.time.DateTime
import java.util.UUID
class InvalidatePendingTransactionsUseCase(
@ -40,12 +41,12 @@ class InvalidatePendingTransactionsUseCase(
newBalancesId: Int,
pendingData: List<Pair<PendingTransaction, BalanceItem>>,
): Pair<List<BalanceItem>, List<PendingTransaction>> {
val balances = realData.associateBy { Triple(it.groupId, it.type, it.amount) }.toMutableMap()
val balances = realData.associateBy { Pair(it.groupId to it.type, it.amount to it.date) }.toMutableMap()
val transactionsToRemove = mutableListOf<PendingTransaction>()
pendingData.forEach { (pendingTransaction, balanceItem) ->
val key = Triple(balanceItem.groupId, balanceItem.type, balanceItem.amount)
val key = Pair(balanceItem.groupId to balanceItem.type, balanceItem.amount to balanceItem.date)
val oldBalancesId = pendingTransaction.balancesId
when {
@ -57,7 +58,8 @@ class InvalidatePendingTransactionsUseCase(
}
else -> {
val groupId = UUID.randomUUID().toString()
balances[Triple(groupId, BalanceType.STAKED, pendingTransaction.amount)] = BalanceItem(
val now = DateTime.now()
balances[Pair(groupId to BalanceType.STAKED, pendingTransaction.amount to now)] = BalanceItem(
groupId = groupId,
type = pendingTransaction.type,
amount = pendingTransaction.amount,

View file

@ -7,6 +7,7 @@ import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.RewardBlockType
import com.tangem.domain.staking.model.stakekit.Yield
import kotlinx.collections.immutable.ImmutableList
import org.joda.time.DateTime
import java.math.BigDecimal
internal sealed class InnerYieldBalanceState {
@ -35,4 +36,5 @@ internal data class BalanceState(
val validator: Yield.Validator?,
val pendingActions: ImmutableList<PendingAction>,
val isPending: Boolean,
val date: DateTime,
)

View file

@ -63,6 +63,7 @@ internal class BalanceItemConverter(
pendingActions = value.pendingActions.toPersistentList(),
isClickable = value.type.isClickable() && !value.isPending,
isPending = value.isPending,
date = value.date ?: DateTime.now(),
)
}
}

View file

@ -14,6 +14,7 @@ import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toPersistentList
import org.joda.time.DateTime
import java.math.BigDecimal
internal class RewardsValidatorStateConverter(
@ -91,6 +92,7 @@ internal class RewardsValidatorStateConverter(
isClickable = true,
type = balance.type,
isPending = balance.isPending,
date = balance.date ?: DateTime.now(),
)
}
}

View file

@ -12,6 +12,7 @@ import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import kotlinx.collections.immutable.persistentListOf
import org.joda.time.DateTime
internal object InitialStakingStatePreview {
val defaultState = StakingStates.InitialInfoState.Data(
@ -93,6 +94,7 @@ internal object InitialStakingStatePreview {
type = BalanceType.STAKED,
subtitle = null,
isPending = false,
date = DateTime.now(),
),
),
),

View file

@ -33,6 +33,8 @@ internal object StakingClickIntentsStub : StakingClickIntents {
override fun onInfoClick(infoType: InfoType) {}
override fun onEnterClick() {}
override fun onAmountValueChange(value: String) {}
override fun onAmountPasteTriggerDismiss() {}

View file

@ -140,10 +140,7 @@ internal class SetButtonsStateTransformer(
private fun StakingUiState.onPrimaryClick() {
when (currentStep) {
StakingStep.InitialInfo -> {
clickIntents.onAmountValueChange("") // reset amount state
clickIntents.onNextClick(StakingActionCommonType.ENTER)
}
StakingStep.InitialInfo -> clickIntents.onEnterClick()
StakingStep.Validators -> {
if (confirmationState is StakingStates.ConfirmationState.Data) {
clickIntents.onNextClick(
@ -154,9 +151,7 @@ internal class SetButtonsStateTransformer(
clickIntents.onNextClick()
}
}
StakingStep.Amount -> {
clickIntents.onNextClick()
}
StakingStep.Amount -> clickIntents.onNextClick()
StakingStep.Confirmation -> onConfirmationClick()
StakingStep.RewardsValidators -> Unit
}

View file

@ -3,7 +3,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.features.staking.impl.presentation.state.ValidatorState
import com.tangem.utils.transformer.Transformer
internal object SetConfirmationStateResetAssentTransformer : Transformer<StakingUiState> {
@ -14,7 +13,7 @@ internal object SetConfirmationStateResetAssentTransformer : Transformer<Staking
confirmationState.copy(
isPrimaryButtonEnabled = true,
innerState = InnerConfirmationStakingState.ASSENT,
validatorState = ValidatorState.Loading,
validatorState = confirmationState.validatorState.copySealed(isClickable = true),
)
} else {
confirmationState

View file

@ -33,6 +33,8 @@ internal interface StakingClickIntents : AmountScreenClickIntents {
fun onInfoClick(infoType: InfoType)
fun onEnterClick()
fun getFee(pendingAction: PendingAction?, pendingActions: ImmutableList<PendingAction>?)
override fun onAmountNext() = onNextClick(actionTypeToOverwrite = null)

View file

@ -388,6 +388,27 @@ internal class StakingViewModel @Inject constructor(
)
}
override fun onEnterClick() {
onAmountValueChange("") // reset amount state
// TODO refactor in [REDACTED_JIRA]
val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data
val filteredValidator = yield.validators.filter { it.preferred }
stateController.update {
value.copy(
confirmationState = confirmationState?.copy(
validatorState = ValidatorState.Content(
isClickable = true,
chosenValidator = filteredValidator[0],
availableValidators = filteredValidator,
),
) as StakingStates.ConfirmationState,
)
}
onNextClick(StakingActionCommonType.ENTER)
}
override fun onAmountValueChange(value: String) {
stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, value, yield))
}

View file

@ -113,8 +113,21 @@ data class TxFee(
val decimals: Int,
val cryptoSymbol: String,
val feeType: FeeType,
val gasPremium: Long?,
)
val params: Params?,
) {
sealed class Params {
data class Filecoin(
val gasPremium: Long,
) : Params()
data class Sui(
val gasBudget: Long,
val gasPrice: Long,
) : Params()
}
}
enum class FeeType {
NORMAL, PRIORITY

View file

@ -867,6 +867,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
}
}
@Suppress("LongMethod")
private fun getFeeForTransaction(fee: TxFee, blockchain: Blockchain): Fee {
val feeAmountValue = fee.feeValue
val feeAmount = Amount(
@ -876,50 +877,192 @@ internal class SwapInteractorImpl @AssistedInject constructor(
type = AmountType.Coin,
)
return when {
blockchain.isEvm() -> {
val feeAmountWithDecimals = feeAmountValue.movePointRight(fee.decimals)
Fee.Ethereum.Legacy(
amount = feeAmount,
gasLimit = fee.gasLimit.toBigInteger(),
gasPrice = (feeAmountWithDecimals / fee.gasLimit.toBigDecimal()).toBigInteger(),
)
}
blockchain == Blockchain.VeChain -> Fee.VeChain(
return if (blockchain.isEvm()) {
val feeAmountWithDecimals = feeAmountValue.movePointRight(fee.decimals)
Fee.Ethereum.Legacy(
amount = feeAmount,
gasPriceCoef = Fee.VeChain.getGasPriceCoef(fee.gasLimit.toLong(), fee.feeValue),
gasLimit = fee.gasLimit.toLong(),
gasLimit = fee.gasLimit.toBigInteger(),
gasPrice = (feeAmountWithDecimals / fee.gasLimit.toBigDecimal()).toBigInteger(),
)
blockchain == Blockchain.Aptos -> {
val gasUnitPrice = fee.feeValue.divide(
fee.gasLimit.toBigDecimal(),
Blockchain.Aptos.decimals(),
RoundingMode.HALF_UP,
)
Fee.Aptos(
amount = feeAmount,
gasUnitPrice = gasUnitPrice
.movePointRight(Blockchain.Aptos.decimals())
.toLong(),
gasLimit = fee.gasLimit.toLong(),
)
} else {
when (blockchain) {
// region Blockchains with their own fees
Blockchain.VeChain,
Blockchain.VeChainTestnet,
-> {
Fee.VeChain(
amount = feeAmount,
gasPriceCoef = Fee.VeChain.getGasPriceCoef(fee.gasLimit.toLong(), fee.feeValue),
gasLimit = fee.gasLimit.toLong(),
)
}
Blockchain.Aptos,
Blockchain.AptosTestnet,
-> {
val gasUnitPrice = fee.feeValue.divide(
fee.gasLimit.toBigDecimal(),
Blockchain.Aptos.decimals(),
RoundingMode.HALF_UP,
)
Fee.Aptos(
amount = feeAmount,
gasUnitPrice = gasUnitPrice
.movePointRight(Blockchain.Aptos.decimals())
.toLong(),
gasLimit = fee.gasLimit.toLong(),
)
}
Blockchain.Filecoin,
-> {
val gasUnitPrice = fee.feeValue.divide(
BigDecimal(fee.gasLimit),
Blockchain.Filecoin.decimals(),
RoundingMode.HALF_UP,
)
val feeParams = requireNotNull(fee.params as? TxFee.Params.Filecoin)
Fee.Filecoin(
amount = feeAmount,
gasUnitPrice = gasUnitPrice
.movePointRight(Blockchain.Filecoin.decimals())
.toLong(),
gasLimit = fee.gasLimit.toLong(),
gasPremium = feeParams.gasPremium,
)
}
Blockchain.Sui,
Blockchain.SuiTestnet,
-> {
val feeParams = requireNotNull(fee.params as? TxFee.Params.Sui)
Fee.Sui(
amount = feeAmount,
gasPrice = feeParams.gasPrice,
gasBudget = feeParams.gasBudget,
)
}
// endregion
// region Blockchains with common fees or EVM-like fees
Blockchain.Unknown,
Blockchain.Arbitrum,
Blockchain.ArbitrumTestnet,
Blockchain.Avalanche,
Blockchain.AvalancheTestnet,
Blockchain.Binance,
Blockchain.BinanceTestnet,
Blockchain.BSC,
Blockchain.BSCTestnet,
Blockchain.Bitcoin,
Blockchain.BitcoinTestnet,
Blockchain.BitcoinCash,
Blockchain.BitcoinCashTestnet,
Blockchain.Cardano,
Blockchain.Cosmos,
Blockchain.CosmosTestnet,
Blockchain.Dogecoin,
Blockchain.Ducatus,
Blockchain.Ethereum,
Blockchain.EthereumTestnet,
Blockchain.EthereumClassic,
Blockchain.EthereumClassicTestnet,
Blockchain.Fantom,
Blockchain.FantomTestnet,
Blockchain.Litecoin,
Blockchain.Near,
Blockchain.NearTestnet,
Blockchain.Polkadot,
Blockchain.PolkadotTestnet,
Blockchain.Kava,
Blockchain.KavaTestnet,
Blockchain.Kusama,
Blockchain.Polygon,
Blockchain.PolygonTestnet,
Blockchain.RSK,
Blockchain.Sei,
Blockchain.SeiTestnet,
Blockchain.Stellar,
Blockchain.StellarTestnet,
Blockchain.Solana,
Blockchain.SolanaTestnet,
Blockchain.Tezos,
Blockchain.Tron,
Blockchain.TronTestnet,
Blockchain.XRP,
Blockchain.Gnosis,
Blockchain.Dash,
Blockchain.Optimism,
Blockchain.OptimismTestnet,
Blockchain.Dischain,
Blockchain.EthereumPow,
Blockchain.EthereumPowTestnet,
Blockchain.Kaspa,
Blockchain.Telos,
Blockchain.TelosTestnet,
Blockchain.TON,
Blockchain.TONTestnet,
Blockchain.Ravencoin,
Blockchain.RavencoinTestnet,
Blockchain.TerraV1,
Blockchain.TerraV2,
Blockchain.Cronos,
Blockchain.AlephZero,
Blockchain.AlephZeroTestnet,
Blockchain.OctaSpace,
Blockchain.OctaSpaceTestnet,
Blockchain.Chia,
Blockchain.ChiaTestnet,
Blockchain.Decimal,
Blockchain.DecimalTestnet,
Blockchain.XDC,
Blockchain.XDCTestnet,
Blockchain.Playa3ull,
Blockchain.Shibarium,
Blockchain.ShibariumTestnet,
Blockchain.Algorand,
Blockchain.AlgorandTestnet,
Blockchain.Hedera,
Blockchain.HederaTestnet,
Blockchain.Aurora,
Blockchain.AuroraTestnet,
Blockchain.Areon,
Blockchain.AreonTestnet,
Blockchain.PulseChain,
Blockchain.PulseChainTestnet,
Blockchain.ZkSyncEra,
Blockchain.ZkSyncEraTestnet,
Blockchain.Nexa,
Blockchain.NexaTestnet,
Blockchain.Moonbeam,
Blockchain.MoonbeamTestnet,
Blockchain.Manta,
Blockchain.MantaTestnet,
Blockchain.PolygonZkEVM,
Blockchain.PolygonZkEVMTestnet,
Blockchain.Radiant,
Blockchain.Base,
Blockchain.BaseTestnet,
Blockchain.Moonriver,
Blockchain.MoonriverTestnet,
Blockchain.Mantle,
Blockchain.MantleTestnet,
Blockchain.Flare,
Blockchain.FlareTestnet,
Blockchain.Taraxa,
Blockchain.TaraxaTestnet,
Blockchain.Koinos,
Blockchain.KoinosTestnet,
Blockchain.Joystream,
Blockchain.Bittensor,
Blockchain.Blast,
Blockchain.BlastTestnet,
Blockchain.Cyber,
Blockchain.CyberTestnet,
Blockchain.InternetComputer,
-> Fee.Common(feeAmount)
// endregion
}
blockchain == Blockchain.Filecoin -> {
val gasUnitPrice = fee.feeValue.divide(
BigDecimal(fee.gasLimit),
Blockchain.Filecoin.decimals(),
RoundingMode.HALF_UP,
)
Fee.Filecoin(
amount = feeAmount,
gasUnitPrice = gasUnitPrice
.movePointRight(Blockchain.Filecoin.decimals())
.toLong(),
gasLimit = fee.gasLimit.toLong(),
gasPremium = requireNotNull(fee.gasPremium),
)
}
else -> Fee.Common(feeAmount)
}
}
@ -1615,7 +1758,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
decimals = minFee.fee.decimals,
cryptoSymbol = minFee.fee.currencySymbol,
feeType = FeeType.NORMAL,
gasPremium = (minFee as? ProxyFee.Filecoin)?.gasPremium,
params = getSwapFeeParams(minFee),
),
priorityFee = TxFee(
feeValue = priorityFeeValue,
@ -1628,7 +1771,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
decimals = normalFee.fee.decimals,
cryptoSymbol = normalFee.fee.currencySymbol,
feeType = FeeType.PRIORITY,
gasPremium = (normalFee as? ProxyFee.Filecoin)?.gasPremium,
params = getSwapFeeParams(normalFee),
),
)
}
@ -1671,7 +1814,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
decimals = singleFee.fee.decimals,
cryptoSymbol = singleFee.fee.currencySymbol,
feeType = FeeType.NORMAL,
gasPremium = (singleFee as? ProxyFee.Filecoin)?.gasPremium,
params = getSwapFeeParams(singleFee),
),
)
}
@ -1727,7 +1870,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
decimals = normalFee.amount.decimals,
cryptoSymbol = normalFee.amount.currencySymbol,
feeType = FeeType.NORMAL,
gasPremium = (normalFee as? Fee.Filecoin)?.gasPremium,
params = getSwapFeeParams(normalFee),
),
priorityFee = TxFee(
feeValue = feePriority,
@ -1740,7 +1883,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
decimals = priorityFee.amount.decimals,
cryptoSymbol = priorityFee.amount.currencySymbol,
feeType = FeeType.PRIORITY,
gasPremium = (priorityFee as? Fee.Filecoin)?.gasPremium,
params = getSwapFeeParams(priorityFee),
),
)
}
@ -1772,13 +1915,46 @@ internal class SwapInteractorImpl @AssistedInject constructor(
decimals = normal.amount.decimals,
cryptoSymbol = normal.amount.currencySymbol,
feeType = FeeType.NORMAL,
gasPremium = (normal as? Fee.Filecoin)?.gasPremium,
params = getSwapFeeParams(normal),
),
)
}
}
}
private fun getSwapFeeParams(fee: Fee): TxFee.Params? = when (fee) {
is Fee.Filecoin -> TxFee.Params.Filecoin(
gasPremium = fee.gasPremium,
)
is Fee.Sui -> TxFee.Params.Sui(
gasPrice = fee.gasPrice,
gasBudget = fee.gasBudget,
)
is Fee.Aptos,
is Fee.Bitcoin,
is Fee.CardanoToken,
is Fee.Common,
is Fee.Ethereum.EIP1559,
is Fee.Ethereum.Legacy,
is Fee.Kaspa,
is Fee.Tron,
is Fee.VeChain,
-> null
}
private fun getSwapFeeParams(proxyFee: ProxyFee): TxFee.Params? = when (proxyFee) {
is ProxyFee.Filecoin -> TxFee.Params.Filecoin(
gasPremium = proxyFee.gasPremium,
)
is ProxyFee.Sui -> TxFee.Params.Sui(
gasPrice = proxyFee.gasPrice,
gasBudget = proxyFee.gasBudget,
)
is ProxyFee.CardanoToken,
is ProxyFee.Common,
-> null
}
private fun createNativeAmountForDex(txValueAmount: String, network: Network): Amount {
val nativeDecimals = Blockchain.fromNetworkId(network.backendId)?.decimals()
?: error("Blockchain not found")

View file

@ -24,4 +24,11 @@ sealed interface ProxyFee {
override val fee: ProxyAmount,
val gasPremium: Long,
) : ProxyFee
data class Sui(
override val gasLimit: BigInteger,
override val fee: ProxyAmount,
val gasPrice: Long,
val gasBudget: Long,
) : ProxyFee
}