Updated on 2026-08-14

This commit is contained in:
Tangem 2024-09-03 13:27:06 +03:00
commit c4a3a8d108
16 changed files with 132 additions and 51 deletions

View file

@ -72,7 +72,7 @@ class WalletConnectSdkHelper {
}
// TODO move fee calculation to SDK getFee() [REDACTED_JIRA]
val gasLimit = getGasLimitFromTx(value, walletManager, transaction)
val gasLimit = getGasLimitFromTx(value, walletManager, transaction, blockchain)
val gasPrice = getGasPrice(walletManager, transaction)
val feeDecimal = (gasLimit * gasPrice).movePointLeft(decimals)
@ -170,6 +170,7 @@ class WalletConnectSdkHelper {
value: BigDecimal,
walletManager: WalletManager,
transaction: WcEthereumTransaction,
blockchain: Blockchain,
): BigDecimal {
return transaction.gas?.hexToBigDecimal()
?: transaction.gasLimit?.hexToBigDecimal()
@ -177,7 +178,7 @@ class WalletConnectSdkHelper {
value = value,
walletManager = walletManager,
transaction = transaction,
)
).increaseForMantleIfNeeded(blockchain)
}
private suspend fun getGasLimitFromBlockchain(
@ -200,6 +201,15 @@ class WalletConnectSdkHelper {
}
}
// TODO Workaround for Mantle. Remove after [REDACTED_JIRA]
private fun BigDecimal.increaseForMantleIfNeeded(blockchain: Blockchain): BigDecimal {
return if (blockchain == Blockchain.Mantle) {
this.multiply(MANTLE_FEE_ESTIMATE_MULTIPLIER)
} else {
this
}
}
private suspend fun sendTransaction(data: WcTransactionData, cardId: String?): String? {
val result = (data.walletManager as TransactionSender).send(
transactionData = data.transaction,
@ -437,6 +447,6 @@ class WalletConnectSdkHelper {
const val HEX_PREFIX = "0x"
const val DEFAULT_MAX_GASLIMIT = 350000
// TODO remove after [REDACTED_JIRA]
private val MANTLE_FEE_ESTIMATE_MULTIPLIER = BigDecimal("1.6")
private val MANTLE_FEE_ESTIMATE_MULTIPLIER = BigDecimal("1.8")
}
}

View file

@ -272,22 +272,24 @@ class TransactionManagerImpl(
* @param blockchain
*/
private fun createMultipleProxyFees(gasPrice: BigInteger, gasLimit: BigInteger, blockchain: Blockchain): ProxyFees {
val gasPriceNormal = gasPrice.increaseBigIntegerByPercents(MULTIPLIER_GAS_PRICE_FOR_NORMAL_FEE)
val patchedGasLimit = gasLimit.toBigDecimal().increaseForMantleIfNeeded(blockchain).toBigInteger()
val gasPriceNormal = gasPrice
.increaseBigIntegerByPercents(MULTIPLIER_GAS_PRICE_FOR_NORMAL_FEE)
val gasPricePriority = gasPrice.increaseBigIntegerByPercents(MULTIPLIER_GAS_PRICE_FOR_PRIORITY_FEE)
val feeMin = gasLimit.multiply(gasPrice).toBigDecimal(
val feeMin = patchedGasLimit.multiply(gasPrice).toBigDecimal(
scale = blockchain.decimals(),
mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN),
)
val feeNormal = gasLimit.multiply(gasPriceNormal).toBigDecimal(
).increaseForMantleIfNeeded(blockchain)
val feeNormal = patchedGasLimit.multiply(gasPriceNormal).toBigDecimal(
scale = blockchain.decimals(),
mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN),
)
val feePriority = gasLimit.multiply(gasPricePriority).toBigDecimal(
).increaseForMantleIfNeeded(blockchain)
val feePriority = patchedGasLimit.multiply(gasPricePriority).toBigDecimal(
scale = blockchain.decimals(),
mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN),
)
).increaseForMantleIfNeeded(blockchain)
val minFee = ProxyFee.Common(
gasLimit = gasLimit,
gasLimit = patchedGasLimit,
fee = ProxyAmount(
currencySymbol = blockchain.currency,
value = feeMin,
@ -295,7 +297,7 @@ class TransactionManagerImpl(
),
)
val normalFee = ProxyFee.Common(
gasLimit = gasLimit,
gasLimit = patchedGasLimit,
fee = ProxyAmount(
currencySymbol = blockchain.currency,
value = feeNormal,
@ -303,7 +305,7 @@ class TransactionManagerImpl(
),
)
val priorityFee = ProxyFee.Common(
gasLimit = gasLimit,
gasLimit = patchedGasLimit,
fee = ProxyAmount(
currencySymbol = blockchain.currency,
value = feePriority,
@ -339,8 +341,18 @@ class TransactionManagerImpl(
}
}
// TODO Workaround for Mantle. Remove after [REDACTED_JIRA]
private fun BigDecimal.increaseForMantleIfNeeded(blockchain: Blockchain): BigDecimal {
return if (blockchain == Blockchain.Mantle) {
this.multiply(MANTLE_FEE_ESTIMATE_MULTIPLIER)
} else {
this
}
}
companion object {
private const val MULTIPLIER_GAS_PRICE_FOR_NORMAL_FEE = 150 // 50%
private const val MULTIPLIER_GAS_PRICE_FOR_PRIORITY_FEE = 200 // 50%
private val MANTLE_FEE_ESTIMATE_MULTIPLIER = BigDecimal("1.8")
}
}

View file

@ -118,6 +118,7 @@ class ResponseCryptoCurrenciesFactory {
// get name and symbol from enum Blockchain until backend renamed
// [REDACTED_JIRA]
Blockchain.Dischain,
Blockchain.Polygon,
-> this.currency
else -> responseToken.symbol
}

View file

@ -48,6 +48,8 @@ sealed class CryptoCurrencyWarning {
data object BeaconChainShutdown : CryptoCurrencyWarning()
data object MigrationMaticToPol : CryptoCurrencyWarning()
/**
* Shows a warning about an available fee resource for a transaction in several blockchains (ex. Koinos)
*/

View file

@ -76,6 +76,7 @@ class GetCurrencyWarningsUseCase(
getNetworkNoAccountWarning(currencyStatus),
getBeaconChainShutdownWarning(currency.network.id),
getAssetRequirementsWarning(userWalletId = userWalletId, currency = currency),
getMigrationFromMaticToPolWarning(currency),
)
}.flowOn(dispatchers.io)
}
@ -304,7 +305,19 @@ class GetCurrencyWarningsUseCase(
}
}
private fun getMigrationFromMaticToPolWarning(currency: CryptoCurrency): CryptoCurrencyWarning? {
return if (currency.symbol == MATIC_SYMBOL && !BlockchainUtils.isPolygonChain(currency.network.id.value)) {
CryptoCurrencyWarning.MigrationMaticToPol
} else {
null
}
}
private fun BigDecimal?.isZero(): Boolean {
return this?.signum() == 0
}
companion object {
private const val MATIC_SYMBOL = "MATIC"
}
}

View file

@ -1120,6 +1120,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
isAllowedToSpend = isAllowedToSpend,
spenderAddress = quoteModel.allowanceContract,
)
if (state !is SwapState.QuotesLoadedState) return state
state.copy(
preparedSwapConfigState = state.preparedSwapConfigState.copy(
isAllowedToSpend = isAllowedToSpend,
@ -1153,18 +1154,32 @@ internal class SwapInteractorImpl @AssistedInject constructor(
}
},
ifLeft = { error ->
val rates = getQuotes(fromToken.currency.id)
val fromTokenSwapInfo = TokenSwapInfo(
tokenAmount = amount,
amountFiat = rates[fromToken.currency.id]?.fiatRate?.multiply(amount.value)
?: BigDecimal.ZERO,
cryptoCurrencyStatus = fromToken,
createSwapErrorWith(
fromToken = fromToken,
amount = amount,
includeFeeInAmount = includeFeeInAmount,
dataError = error,
)
return SwapState.SwapError(fromTokenSwapInfo, error, includeFeeInAmount)
},
)
}
private suspend fun createSwapErrorWith(
fromToken: CryptoCurrencyStatus,
amount: SwapAmount,
includeFeeInAmount: IncludeFeeInAmount,
dataError: DataError,
): SwapState.SwapError {
val rates = getQuotes(fromToken.currency.id)
val fromTokenSwapInfo = TokenSwapInfo(
tokenAmount = amount,
amountFiat = rates[fromToken.currency.id]?.fiatRate?.multiply(amount.value)
?: BigDecimal.ZERO,
cryptoCurrencyStatus = fromToken,
)
return SwapState.SwapError(fromTokenSwapInfo, dataError, includeFeeInAmount)
}
@Suppress("CyclomaticComplexMethod")
private suspend fun getIncludeFeeInAmount(
networkId: String,
@ -1460,7 +1475,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
quotesLoadedState: SwapState.QuotesLoadedState,
spenderAddress: String?,
isAllowedToSpend: Boolean,
): SwapState.QuotesLoadedState {
): SwapState {
val fromToken = fromTokenStatus.currency
if (isAllowedToSpend) {
return quotesLoadedState.copy(
@ -1504,15 +1519,19 @@ internal class SwapInteractorImpl @AssistedInject constructor(
)
} catch (e: Exception) {
Timber.e(e, "Failed to get fee")
null
// it's impossible next steps without fee
return createSwapErrorWith(
fromToken = fromTokenStatus,
amount = swapAmount,
includeFeeInAmount = IncludeFeeInAmount.Excluded,
dataError = DataError.UnknownError,
)
}
}
val feeState = feeData?.let {
when (feeData) {
is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(fromToken)
is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(fromToken)
}
} ?: TxFeeState.Empty
val feeState = when (feeData) {
is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(fromToken)
is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(fromToken)
}
val fee = when (feeState) {
TxFeeState.Empty -> BigDecimal.ZERO
is TxFeeState.MultipleFeeState -> feeState.normalFee.feeValue

View file

@ -28,7 +28,7 @@ import com.tangem.feature.swap.presentation.R
fun SwapSuccessScreen(state: SwapSuccessStateHolder, onBack: () -> Unit) {
Scaffold(
modifier = Modifier.systemBarsPadding(),
backgroundColor = TangemTheme.colors.background.tertiary,
backgroundColor = TangemTheme.colors.background.secondary,
content = { padding ->
SwapSuccessScreenContent(padding = padding, state = state)
},

View file

@ -49,6 +49,7 @@ internal class TokenDetailsNotificationsAnalyticsSender(
is TokenDetailsNotification.NetworkShutdown,
is TokenDetailsNotification.HederaAssociateWarning,
is TokenDetailsNotification.KoinosMana,
is TokenDetailsNotification.MigrationMaticToPol,
-> null
}
}

View file

@ -201,4 +201,9 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) {
formatArgs = wrappedList(manaBalanceAmount, maxManaBalanceAmount),
),
)
data object MigrationMaticToPol : Warning(
title = resourceReference(id = R.string.warning_matic_migration_title),
subtitle = resourceReference(id = R.string.warning_matic_migration_message),
)
}

View file

@ -122,6 +122,7 @@ internal class TokenDetailsNotificationConverter(
""
},
)
is CryptoCurrencyWarning.MigrationMaticToPol -> MigrationMaticToPol
}
}

View file

@ -30,6 +30,9 @@ internal class WalletStateController @Inject constructor() {
val value: WalletScreenState get() = uiState.value
val isInitialized: Boolean
get() = value.selectedWalletIndex != NOT_INITIALIZED_WALLET_INDEX
private val mutableUiState: MutableStateFlow<WalletScreenState> = MutableStateFlow(value = getInitialState())
fun update(function: (WalletScreenState) -> WalletScreenState) {

View file

@ -2,8 +2,11 @@ package com.tangem.feature.wallet.presentation.wallet.ui
import android.content.res.Configuration
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.TweenSpec
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
@ -220,12 +223,9 @@ private fun WalletContent(
}
if (marketsEntryComponent != null) {
val bottomSheetState = remember {
mutableStateOf(BottomSheetState.COLLAPSED)
}
var headerSize by remember {
mutableStateOf(0.dp)
}
val bottomSheetState = remember { mutableStateOf(COLLAPSED) }
var headerSize by remember { mutableStateOf(0.dp) }
BaseScaffoldWithMarkets(
state = state,
@ -241,17 +241,15 @@ private fun WalletContent(
modifier = Modifier,
)
},
) {
scaffoldContent()
}
content = scaffoldContent,
)
} else {
BaseScaffold(
state = state,
selectedWallet = selectedWallet,
snackbarHostState = snackbarHostState,
) {
scaffoldContent()
}
content = scaffoldContent,
)
}
}
@ -274,16 +272,21 @@ private fun BaseScaffold(
)
},
floatingActionButton = {
val manageTokensButtonConfig by remember(state.selectedWalletIndex) {
mutableStateOf(
(state.wallets[state.selectedWalletIndex] as? WalletState.MultiCurrency)?.manageTokensButtonConfig,
)
}
val manageTokensButtonConfig by rememberUpdatedState(
newValue = (state.wallets[state.selectedWalletIndex] as? WalletState.MultiCurrency)
?.manageTokensButtonConfig,
)
AnimatedVisibility(
visible = manageTokensButtonConfig != null,
enter = fadeIn(),
exit = fadeOut(),
) {
val config = manageTokensButtonConfig ?: return@AnimatedVisibility
manageTokensButtonConfig?.let {
ManageTokensButton(
modifier = Modifier.navigationBarsPadding(),
onClick = it.onClick,
onClick = config.onClick,
)
}
},

View file

@ -7,7 +7,10 @@ import androidx.lifecycle.viewModelScope
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.settings.*
import com.tangem.domain.settings.CanUseBiometryUseCase
import com.tangem.domain.settings.IsWalletsScrollPreviewEnabled
import com.tangem.domain.settings.ShouldAskPermissionUseCase
import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase
import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
@ -190,6 +193,8 @@ internal class WalletViewModel @Inject constructor(
/** Change selected wallet state if selected wallet [selectedWalletId] was changed in the background */
private suspend fun changeSelectedWalletState(selectedWalletId: UserWalletId) {
if (!stateHolder.isInitialized) return
if (screenLifecycleProvider.isBackgroundState.value && selectedWalletId != stateHolder.getSelectedWalletId()) {
stateHolder.value.wallets
.indexOfFirstOrNull { prevState -> prevState.walletCardState.id == selectedWalletId }

View file

@ -88,7 +88,7 @@ markdown = "0.7.2"
# endregion Other libraries
# region Tangem
tangemBlockchainSdk = "develop-755"
tangemBlockchainSdk = "develop-759"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-378"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^

View file

@ -332,7 +332,7 @@ fun Blockchain.toCoinId(): String {
Blockchain.Bittensor -> "bittensor"
Blockchain.Filecoin -> "filecoin"
Blockchain.Blast, Blockchain.BlastTestnet -> "blast-ethereum"
Blockchain.Cyber, Blockchain.CyberTestnet -> "cyberconnect"
Blockchain.Cyber, Blockchain.CyberTestnet -> "cyber-ethereum"
Blockchain.Sei, Blockchain.SeiTestnet -> "sei-network"
}
}

View file

@ -52,6 +52,12 @@ object BlockchainUtils {
return blockchain == Blockchain.Binance || blockchain == Blockchain.BinanceTestnet
}
/** If current [networkId] is Polygon */
fun isPolygonChain(networkId: String): Boolean {
val blockchain = Blockchain.fromId(networkId)
return blockchain == Blockchain.Polygon || blockchain == Blockchain.PolygonTestnet
}
fun isTron(networkId: String): Boolean {
val blockchain = Blockchain.fromId(networkId)
return blockchain == Blockchain.Tron || blockchain == Blockchain.TronTestnet