Updated on 2026-08-14

This commit is contained in:
Tangem 2023-05-23 19:46:52 +03:00
parent c1f4cb7388
commit 9c762583a0
18 changed files with 759 additions and 217 deletions

View file

@ -2,6 +2,10 @@
<code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="120" />
<option name="SOFT_MARGINS" value="120" />
<JavaCodeStyleSettings>
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="5" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="3" />
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>

View file

@ -67,6 +67,7 @@ sealed class CardInfo(
)
}
// TODO("Remove and use the same from coreUI")
sealed interface TextReference {
class Res(@StringRes val id: Int, val formatArgs: List<Any> = emptyList()) : TextReference {
constructor(@StringRes id: Int, vararg formatArgs: Any) : this(id, formatArgs.toList())

View file

@ -5,14 +5,7 @@ import com.tangem.Message
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.blockchains.optimism.OptimismWalletManager
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.TransactionExtras
import com.tangem.blockchain.common.TransactionSender
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.blockchain.extensions.isNetworkError
@ -21,10 +14,7 @@ import com.tangem.common.extensions.hexToBytes
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.models.Currency
import com.tangem.lib.crypto.models.ProxyAmount
import com.tangem.lib.crypto.models.ProxyFee
import com.tangem.lib.crypto.models.ProxyNetworkInfo
import com.tangem.lib.crypto.models.*
import com.tangem.lib.crypto.models.transactions.SendTxResult
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Basic
@ -152,7 +142,7 @@ class TransactionManagerImpl(
increaseBy: Int?,
data: String?,
derivationPath: String?,
): ProxyFee {
): ProxyFees {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain, derivationPath)
if (walletManager is EthereumWalletManager) {
@ -199,7 +189,7 @@ class TransactionManagerImpl(
currency: Currency,
blockchain: Blockchain,
destinationAddress: String,
): ProxyFee {
): ProxyFees {
val fee = (walletManager as? TransactionSender)?.getFee(
amount = createAmount(amountToSend, currency, blockchain),
destination = destinationAddress,
@ -207,9 +197,23 @@ class TransactionManagerImpl(
return when (fee) {
is Result.Success -> {
// for not EVM blockchains set gasLimit ZERO for now
ProxyFee(
val firstFee = fee.data.firstOrNull() ?: error("no fee found")
val minFee = ProxyFee(
gasLimit = BigInteger.ZERO,
fee = convertToProxyAmount(fee.data.firstOrNull() ?: error("no fee found")),
fee = convertToProxyAmount(amount = firstFee),
)
val normalFee = ProxyFee(
gasLimit = BigInteger.ZERO,
fee = convertToProxyAmount(fee.data.getOrNull(index = 1) ?: firstFee),
)
val priorityFee = ProxyFee(
gasLimit = BigInteger.ZERO,
fee = convertToProxyAmount(fee.data.getOrNull(index = 2) ?: firstFee),
)
ProxyFees(
minFee = minFee,
normalFee = normalFee,
priorityFee = priorityFee,
)
}
is Result.Failure -> {
@ -227,7 +231,7 @@ class TransactionManagerImpl(
destinationAddress: String,
data: String?,
increaseBy: Int?,
): ProxyFee {
): ProxyFees {
val gasLimit = getGasLimit(
evmWalletManager = walletManager,
blockchain = blockchain,
@ -235,27 +239,10 @@ class TransactionManagerImpl(
currency = currency,
destinationAddress = destinationAddress,
data = data,
).let {
if (increaseBy != null && increaseBy != 0) {
it.multiply(increaseBy.toBigInteger()).divide(BigInteger("100"))
} else {
it
}
}
).increaseBigIntegerByPercents(increaseBy)
return when (val gasPrice = walletManager.getGasPrice()) {
is Result.Success -> {
val fee = gasLimit.multiply(gasPrice.data).toBigDecimal(
scale = blockchain.decimals(),
mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN),
)
ProxyFee(
gasLimit = gasLimit,
fee = ProxyAmount(
currencySymbol = blockchain.currency,
value = fee,
decimals = blockchain.decimals(),
),
)
createProxyFees(gasPrice = gasPrice.data, gasLimit = gasLimit, blockchain = blockchain)
}
is Result.Failure -> {
error(gasPrice.error.message ?: gasPrice.error.customMessage)
@ -268,17 +255,31 @@ class TransactionManagerImpl(
amount: Amount,
destinationAddress: String,
data: String?,
): ProxyFee {
): ProxyFees {
val fee = if (data.isNullOrEmpty()) {
walletManager.getFee(amount, destinationAddress)
} else {
walletManager.getFee(amount, destinationAddress, data)
}
when (fee) {
return when (fee) {
is Result.Success -> {
return ProxyFee(
val minFee = fee.data.firstOrNull() ?: error("no fee found")
val minProxyFee = ProxyFee(
gasLimit = walletManager.gasLimit ?: BigInteger.ZERO,
fee = convertToProxyAmount(fee.data.lastOrNull() ?: error("no fee found")),
fee = convertToProxyAmount(minFee),
)
val normalProxyFee = ProxyFee(
gasLimit = walletManager.gasLimit ?: BigInteger.ZERO,
fee = convertToProxyAmount(fee.data.getOrNull(index = 1) ?: minFee),
)
val priorityProxyFee = ProxyFee(
gasLimit = walletManager.gasLimit ?: BigInteger.ZERO,
fee = convertToProxyAmount(fee.data.lastOrNull() ?: minFee),
)
ProxyFees(
minFee = minProxyFee,
normalFee = normalProxyFee,
priorityFee = priorityProxyFee,
)
}
is Result.Failure -> {
@ -397,6 +398,59 @@ class TransactionManagerImpl(
}
}
/**
* Create proxy fees
*
* @param gasPrice min fee gasPrice
* @param gasLimit
* @param blockchain
*/
private fun createProxyFees(gasPrice: BigInteger, gasLimit: BigInteger, blockchain: Blockchain): ProxyFees {
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(
scale = blockchain.decimals(),
mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN),
)
val feeNormal = gasLimit.multiply(gasPriceNormal).toBigDecimal(
scale = blockchain.decimals(),
mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN),
)
val feePriority = gasLimit.multiply(gasPricePriority).toBigDecimal(
scale = blockchain.decimals(),
mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN),
)
val minFee = ProxyFee(
gasLimit = gasLimit,
fee = ProxyAmount(
currencySymbol = blockchain.currency,
value = feeMin,
decimals = blockchain.decimals(),
),
)
val normalFee = ProxyFee(
gasLimit = gasLimit,
fee = ProxyAmount(
currencySymbol = blockchain.currency,
value = feeNormal,
decimals = blockchain.decimals(),
),
)
val priorityFee = ProxyFee(
gasLimit = gasLimit,
fee = ProxyAmount(
currencySymbol = blockchain.currency,
value = feePriority,
decimals = blockchain.decimals(),
),
)
return ProxyFees(
minFee = minFee,
normalFee = normalFee,
priorityFee = priorityFee,
)
}
private fun createAmount(amount: BigDecimal, currency: Currency, blockchain: Blockchain): Amount {
return when (currency) {
is Currency.NativeToken -> {
@ -445,8 +499,24 @@ class TransactionManagerImpl(
)
}
/**
* Increase big integer by percents
*
* @param percents in format 150 -> 50%
* @return increased value
*/
private fun BigInteger.increaseBigIntegerByPercents(percents: Int?): BigInteger {
return if (percents != null && percents != 0) {
this.multiply(percents.toBigInteger()).divide(BigInteger("100"))
} else {
this
}
}
companion object {
private const val HEX_PREFIX = "0x"
private const val USER_CANCELLED_ERROR_CODE = 50002
private const val MULTIPLIER_GAS_PRICE_FOR_NORMAL_FEE = 150 // 50%
private const val MULTIPLIER_GAS_PRICE_FOR_PRIORITY_FEE = 200 // 50%
}
}

View file

@ -17,6 +17,7 @@ dependencies {
implementation(deps.compose.accompanist.systemUiController)
implementation(deps.material)
implementation(deps.compose.shimmer)
implementation(deps.kotlin.immutable.collections)
implementation(project(":core:res"))
}

View file

@ -3,34 +3,27 @@ package com.tangem.core.ui.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.states.Item
import com.tangem.core.ui.components.states.SelectableItemsState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.valentinilk.shimmer.shimmer
import kotlinx.collections.immutable.toImmutableList
/**
* Small card with text information attached to the edges
@ -116,40 +109,7 @@ fun SmallInfoCardWithWarning(startText: String, endText: String, warningText: St
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12),
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
top = TangemTheme.dimens.spacing14,
bottom = TangemTheme.dimens.spacing16,
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.background(
color = TangemTheme.colors.background.secondary,
shape = CircleShape,
)
.height(TangemTheme.dimens.size40)
.width(TangemTheme.dimens.size40),
contentAlignment = Alignment.Center,
) {
Image(
painter = painterResource(id = R.drawable.img_attention_20),
contentDescription = null,
)
}
SpacerW12()
Text(
text = warningText,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.body2,
)
}
WarningItem(warningText = warningText)
}
}
}
@ -257,6 +217,72 @@ fun IconWithTitleAndDescription(
}
}
/**
* Card with text information attached to the edges and selectable items
*
* @param state state of block with items
* @param isLoading if true, shimmer is shown instead of data
* @param disclaimer description text
* @param onSelect listener on item selection
*
* @see <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=290-217&t=FMnRqkWPAgdZSdhv-0"
* >Figma component</a>
*/
@Composable
fun <T> SelectableInfoCard(
state: SelectableItemsState<T>,
onSelect: (Item<T>) -> Unit,
isLoading: Boolean = false,
disclaimer: String? = null,
) {
Surface(
shape = RoundedCornerShape(TangemTheme.dimens.radius12),
color = TangemTheme.colors.background.primary,
elevation = TangemTheme.dimens.elevation2,
) {
SelectableInfoBlock(
state = state,
isLoading = isLoading,
disclaimer = disclaimer,
onSelect = onSelect,
)
}
}
/**
* Card with text information attached to the edges, selectable items and warning
*
* @param state state of block with items
* @param isLoading if true, shimmer is shown instead of data
* @param disclaimer description text
* @param onSelect listener on item selection
*
* @see <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=290-217&t=FMnRqkWPAgdZSdhv-0"
* >Figma component</a>
*/
@Composable
fun <T> SelectableInfoCardWithWarning(
state: SelectableItemsState<T>,
warningText: String,
onSelect: (Item<T>) -> Unit,
isLoading: Boolean = false,
disclaimer: String? = null,
) {
Surface(
shape = RoundedCornerShape(TangemTheme.dimens.radius12),
color = TangemTheme.colors.background.primary,
elevation = TangemTheme.dimens.elevation2,
) {
SelectableInfoBlock(
state = state,
isLoading = isLoading,
disclaimer = disclaimer,
warningText = warningText,
onSelect = onSelect,
)
}
}
// region elements
@Composable
@ -314,6 +340,139 @@ private fun CardInfoBox(startText: String, endText: String, disclaimer: String?,
}
}
@Suppress("LongMethod")
@Composable
private fun <T> SelectableInfoBlock(
state: SelectableItemsState<T>,
onSelect: (Item<T>) -> Unit,
isLoading: Boolean = false,
disclaimer: String? = null,
warningText: String? = null,
) {
var isOpened by remember { mutableStateOf(false) }
Column {
if (isLoading) {
ShimmerItem(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12)
} else {
val selectedItem = state.selectedItem
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(TangemTheme.dimens.size48)
.clickable { isOpened = !isOpened }
.padding(
horizontal = TangemTheme.dimens.spacing16,
vertical = TangemTheme.dimens.spacing12,
),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = selectedItem.startText.resolveReference(),
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
style = TangemTheme.typography.subtitle2,
)
Row {
Text(
text = selectedItem.endText.resolveReference(),
color = TangemTheme.colors.text.primary1,
maxLines = 1,
style = TangemTheme.typography.body2,
)
val chevronIcon = if (isOpened) {
painterResource(id = R.drawable.ic_chevron_up_24)
} else {
painterResource(id = R.drawable.ic_chevron_24)
}
Icon(
modifier = Modifier.size(TangemTheme.dimens.size20),
painter = chevronIcon,
tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
)
}
}
Divider(
color = TangemTheme.colors.stroke.primary,
thickness = TangemTheme.dimens.size0_5,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12),
)
if (isOpened) {
state.items.forEach { item ->
SelectableItem(item) {
onSelect.invoke(item)
}
Divider(
color = TangemTheme.colors.stroke.primary,
thickness = TangemTheme.dimens.size0_5,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12),
)
}
if (disclaimer != null) {
DisclaimerItem(disclaimer)
Divider(
color = TangemTheme.colors.stroke.primary,
thickness = TangemTheme.dimens.size0_5,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12),
)
}
}
if (warningText?.isNotEmpty() == true) {
WarningItem(warningText = warningText)
}
}
}
}
@Composable
private fun <T> SelectableItem(item: Item<T>, onSelect: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(TangemTheme.dimens.size48)
.clickable { onSelect() }
.padding(
horizontal = TangemTheme.dimens.spacing16,
vertical = TangemTheme.dimens.spacing12,
),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = item.startText.resolveReference(),
color = TangemTheme.colors.text.primary1,
maxLines = 1,
style = TangemTheme.typography.subtitle2,
)
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = item.endText.resolveReference(),
color = TangemTheme.colors.text.primary1,
maxLines = 1,
style = TangemTheme.typography.body2,
)
val size = TangemTheme.dimens.size24
if (item.isSelected) {
Icon(
modifier = Modifier.size(size),
painter = painterResource(id = R.drawable.ic_check_24),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
)
} else {
SpacerW(width = size)
}
}
}
}
@Composable
private fun ShimmerItem(width: Dp, height: Dp) {
Box(
@ -372,6 +531,44 @@ private fun DisclaimerItem(disclaimer: String) {
}
}
@Composable
private fun WarningItem(warningText: String) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
top = TangemTheme.dimens.spacing14,
bottom = TangemTheme.dimens.spacing16,
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.background(
color = TangemTheme.colors.background.secondary,
shape = CircleShape,
)
.height(TangemTheme.dimens.size40)
.width(TangemTheme.dimens.size40),
contentAlignment = Alignment.Center,
) {
Image(
painter = painterResource(id = R.drawable.img_attention_20),
contentDescription = null,
)
}
SpacerW12()
Text(
text = warningText,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.body2,
)
}
}
// endregion elements
// region Preview
@ -418,6 +615,33 @@ private fun CardsPreview() {
)
},
)
SpacerH32()
val state = SelectableItemsState(
selectedItem = Item(0, TextReference.Str("Balance"), TextReference.Str("0.4405434 BTC"), true, ""),
items = listOf(
Item(0, TextReference.Str("Normal"), TextReference.Str("0.4405434 BTC"), true, ""),
Item(1, TextReference.Str("Priority"), TextReference.Str("0.46 BTC"), false, ""),
).toImmutableList(),
)
SelectableInfoCard(
state = state,
isLoading = false,
disclaimer = "Not enough funds for fee on your Polygon wallet to create a transaction",
onSelect = {},
)
SpacerH32()
SelectableInfoCardWithWarning(
state = state,
isLoading = false,
warningText = "Not enough funds for fee on your Polygon wallet to create a transaction. " +
"Top up your Polygon wallet first.",
disclaimer = "Not enough funds for fee on your Polygon wallet to create a transaction",
onSelect = {},
)
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.core.ui.components.states
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
data class SelectableItemsState<T>(
val selectedItem: Item<T>,
val items: ImmutableList<Item<T>>,
)
data class Item<T>(
val id: Int,
val startText: TextReference,
val endText: TextReference,
val isSelected: Boolean,
val data: T,
)

View file

@ -0,0 +1,23 @@
package com.tangem.core.ui.extensions
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.res.stringResource
sealed interface TextReference {
class Res(@StringRes val id: Int, val formatArgs: List<Any> = emptyList()) : TextReference {
constructor(@StringRes id: Int, vararg formatArgs: Any) : this(id, formatArgs.toList())
}
class Str(val value: String) : TextReference
}
@Composable
@ReadOnlyComposable
fun TextReference.resolveReference(): String {
return when (this) {
is TextReference.Res -> stringResource(this.id, *this.formatArgs.toTypedArray())
is TextReference.Str -> this.value
}
}

View file

@ -80,8 +80,10 @@ interface SwapInteractor {
* @param currencyToSend [Currency]
* @param currencyToGet [Currency]
* @param amountToSwap amount to swap
* @param fee for tx
* @return [TxState]
*/
@Suppress("LongParameterList")
@Throws(IllegalStateException::class)
suspend fun onSwap(
networkId: String,
@ -89,6 +91,7 @@ interface SwapInteractor {
currencyToSend: Currency,
currencyToGet: Currency,
amountToSwap: String,
fee: TxFee,
): TxState
/**

View file

@ -11,6 +11,7 @@ import com.tangem.feature.swap.domain.models.toStringWithRightOffset
import com.tangem.feature.swap.domain.models.ui.*
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.lib.crypto.models.ProxyFees
import com.tangem.lib.crypto.models.ProxyFiatCurrency
import com.tangem.lib.crypto.models.transactions.SendTxResult
import com.tangem.utils.toFiatString
@ -188,14 +189,15 @@ internal class SwapInteractorImpl @Inject constructor(
currencyToSend: Currency,
currencyToGet: Currency,
amountToSwap: String,
fee: TxFee,
): TxState {
val amount = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format, use only digits" }
val result = transactionManager.sendTransaction(
networkId = networkId,
amountToSend = amount,
currencyToSend = cryptoCurrencyConverter.convert(currencyToSend),
feeAmount = swapStateData.fee,
gasLimit = swapStateData.gasLimit,
feeAmount = fee.feeValue,
gasLimit = fee.gasLimit,
destinationAddress = swapStateData.swapModel.transaction.toWalletAddress,
dataToSign = swapStateData.swapModel.transaction.data,
isSwap = true,
@ -346,7 +348,6 @@ internal class SwapInteractorImpl @Inject constructor(
fromTokenAmount = quoteDataModel.fromTokenAmount,
toTokenAmount = quoteDataModel.toTokenAmount,
swapStateData = null,
formattedFee = null,
)
val quotesState = updatePermissionState(
networkId = networkId,
@ -366,12 +367,14 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
private suspend fun getFormattedFiatFee(networkId: String, fee: BigDecimal): String {
private suspend fun getFormattedFiatFees(networkId: String, vararg fees: BigDecimal): List<String> {
val appCurrency = userWalletManager.getUserAppCurrency()
val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId)
val rates = repository.getRates(appCurrency.code, listOf(nativeToken.id))
return rates[nativeToken.id]?.toBigDecimal()?.let { rate ->
" (${fee.toFiatString(rate, appCurrency.symbol, true)})"
fees.map { fee ->
" (${fee.toFiatString(rate, appCurrency.symbol, true)})"
}
}.orEmpty()
}
@ -406,15 +409,11 @@ internal class SwapInteractorImpl @Inject constructor(
data = swapData.transaction.data,
derivationPath = derivationPath,
)
val feeFiat = getFormattedFiatFee(networkId, feeData.fee.value)
val formattedFee = amountFormatter.formatBigDecimalAmountToUI(
amount = feeData.fee.value,
decimals = transactionManager.getNativeTokenDecimals(networkId),
currency = userWalletManager.getNetworkCurrency(networkId),
) + feeFiat
val isBalanceIncludeFeeEnough = isBalanceEnough(networkId, fromToken, amount, feeData.fee.value)
val txFeeState = proxyFeesToFeeState(networkId, feeData)
val isBalanceIncludeFeeEnough =
isBalanceEnough(networkId, fromToken, amount, txFeeState.priorityFee.feeValue)
val isFeeEnough = checkFeeIsEnough(
fee = feeData.fee.value,
fee = txFeeState.priorityFee.feeValue,
spendAmount = amount,
networkId = networkId,
fromToken = fromToken,
@ -425,10 +424,8 @@ internal class SwapInteractorImpl @Inject constructor(
toToken = toToken,
fromTokenAmount = swapData.fromTokenAmount,
toTokenAmount = swapData.toTokenAmount,
formattedFee = formattedFee,
swapStateData = SwapStateData(
gasLimit = feeData.gasLimit.toInt(),
fee = feeData.fee.value,
fee = txFeeState,
swapModel = swapData,
),
)
@ -453,7 +450,6 @@ internal class SwapInteractorImpl @Inject constructor(
toToken: Currency,
fromTokenAmount: SwapAmount,
toTokenAmount: SwapAmount,
formattedFee: String?,
swapStateData: SwapStateData?,
): SwapState.QuotesLoadedState {
val appCurrency = userWalletManager.getUserAppCurrency()
@ -484,7 +480,6 @@ internal class SwapInteractorImpl @Inject constructor(
formatWithSpaces = true,
),
),
fee = formattedFee,
priceImpact = calculatePriceImpact(
fromTokenAmount = fromTokenAmount.value,
fromRate = rates[fromToken.id] ?: 0.0,
@ -531,20 +526,19 @@ internal class SwapInteractorImpl @Inject constructor(
data = transactionData.data,
derivationPath = derivationPath,
)
val feeFiat = getFormattedFiatFee(networkId, feeData.fee.value)
val feeFiat = getFormattedFiatFees(networkId, feeData.normalFee.fee.value)
val formattedFee = amountFormatter.formatBigDecimalAmountToUI(
amount = feeData.fee.value,
amount = feeData.normalFee.fee.value,
decimals = transactionManager.getNativeTokenDecimals(networkId),
currency = userWalletManager.getNetworkCurrency(networkId),
) + feeFiat
) + (feeFiat.firstOrNull() ?: "")
val isFeeEnough = checkFeeIsEnough(
fee = feeData.fee.value,
fee = feeData.normalFee.fee.value,
spendAmount = SwapAmount.zeroSwapAmount(),
networkId = networkId,
fromToken = fromToken,
)
return quotesLoadedState.copy(
fee = formattedFee,
permissionState = PermissionDataState.PermissionReadyForRequest(
currency = fromToken.symbol,
amount = INFINITY_SYMBOL,
@ -552,8 +546,8 @@ internal class SwapInteractorImpl @Inject constructor(
spenderAddress = transactionData.toAddress,
fee = formattedFee,
requestApproveData = RequestApproveStateData(
fee = feeData.fee.value,
gasLimit = feeData.gasLimit.toInt(),
fee = feeData.normalFee.fee.value,
gasLimit = feeData.normalFee.gasLimit.toInt(),
approveModel = transactionData,
),
),
@ -580,6 +574,38 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
private suspend fun proxyFeesToFeeState(networkId: String, proxyFees: ProxyFees): TxFeeState {
val normalFeeValue = proxyFees.normalFee.fee.value
val priorityFeeValue = proxyFees.priorityFee.fee.value
val feesFiat = getFormattedFiatFees(networkId, normalFeeValue, priorityFeeValue)
val normalFiatFee = requireNotNull(feesFiat.getOrNull(0)) { "feesFiat item 0 couldn't be null" }
val priorityFiatFee = requireNotNull(feesFiat.getOrNull(1)) { "feesFiat item 1 couldn't be null" }
val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI(
amount = normalFeeValue,
decimals = transactionManager.getNativeTokenDecimals(networkId),
currency = userWalletManager.getNetworkCurrency(networkId),
)
val priorityCryptoFee = amountFormatter.formatBigDecimalAmountToUI(
amount = priorityFeeValue,
decimals = transactionManager.getNativeTokenDecimals(networkId),
currency = userWalletManager.getNetworkCurrency(networkId),
)
return TxFeeState(
normalFee = TxFee(
feeValue = normalFeeValue,
gasLimit = proxyFees.normalFee.gasLimit.toInt(),
feeFiatFormatted = normalFiatFee,
feeCryptoFormatted = normalCryptoFee,
),
priorityFee = TxFee(
feeValue = priorityFeeValue,
gasLimit = proxyFees.priorityFee.gasLimit.toInt(),
feeFiatFormatted = priorityFiatFee,
feeCryptoFormatted = priorityCryptoFee,
),
)
}
private fun isBalanceEnough(
networkId: String,
fromToken: Currency,
@ -654,7 +680,7 @@ internal class SwapInteractorImpl @Inject constructor(
private const val ZERO_BALANCE = "0"
private const val DEFAULT_BLOCKCHAIN_INCH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
private const val INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT = 1.4
private const val INCREASE_GAS_LIMIT_BY = 125
private const val INCREASE_GAS_LIMIT_BY = 125 // 25%
private const val USDT_SYMBOL = "USDT"
private const val USDC_SYMBOL = "USDC"
private const val INFINITY_SYMBOL = ""

View file

@ -12,7 +12,6 @@ sealed interface SwapState {
data class QuotesLoadedState(
val fromTokenInfo: TokenSwapInfo,
val toTokenInfo: TokenSwapInfo,
val fee: String?,
val priceImpact: Float,
val networkCurrency: String,
val preparedSwapConfigState: PreparedSwapConfigState = PreparedSwapConfigState(
@ -66,7 +65,18 @@ data class RequestApproveStateData(
)
data class SwapStateData(
val fee: BigDecimal,
val gasLimit: Int,
val fee: TxFeeState,
val swapModel: SwapDataModel,
)
data class TxFeeState(
val normalFee: TxFee,
val priorityFee: TxFee,
)
data class TxFee(
val feeValue: BigDecimal,
val gasLimit: Int,
val feeFiatFormatted: String,
val feeCryptoFormatted: String,
)

View file

@ -1,6 +1,9 @@
package com.tangem.feature.swap.models
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.core.ui.components.states.Item
import com.tangem.core.ui.components.states.SelectableItemsState
import com.tangem.feature.swap.domain.models.ui.TxFee
data class SwapStateHolder(
val sendCardData: SwapCardData,
@ -54,12 +57,17 @@ sealed class FeeState(open val tangemFee: Double) {
data class Loaded(
override val tangemFee: Double,
val fee: String = "",
val state: SelectableItemsState<TxFee>?,
val onSelectItem: (Item<TxFee>) -> Unit,
) : FeeState(tangemFee)
object Loading : FeeState(0.0)
data class NotEnoughFundsWarning(override val tangemFee: Double, val fee: String) : FeeState(tangemFee)
data class NotEnoughFundsWarning(
override val tangemFee: Double,
val state: SelectableItemsState<TxFee>?,
val onSelectItem: (Item<TxFee>) -> Unit,
) : FeeState(tangemFee)
}
sealed interface TransactionCardType {

View file

@ -1,5 +1,8 @@
package com.tangem.feature.swap.models
import com.tangem.core.ui.components.states.Item
import com.tangem.feature.swap.domain.models.ui.TxFee
data class UiActions(
val onSearchEntered: (String) -> Unit,
val onSearchFocusChange: (Boolean) -> Unit,
@ -14,4 +17,5 @@ data class UiActions(
val openPermissionBottomSheet: () -> Unit,
val hidePermissionBottomSheet: () -> Unit,
val onChangeApproveType: (ApproveType) -> Unit,
val onSelectItemFee: (Item<TxFee>) -> Unit,
)

View file

@ -2,17 +2,20 @@ package com.tangem.feature.swap.ui
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.core.ui.components.states.Item
import com.tangem.core.ui.components.states.SelectableItemsState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.feature.swap.converters.TokensDataConverter
import com.tangem.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.domain.NetworkInfo
import com.tangem.feature.swap.domain.models.domain.isNonNative
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
import com.tangem.feature.swap.domain.models.ui.FoundTokensState
import com.tangem.feature.swap.domain.models.ui.PermissionDataState
import com.tangem.feature.swap.domain.models.ui.SwapState
import com.tangem.feature.swap.domain.models.ui.TxState
import com.tangem.feature.swap.domain.models.ui.*
import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.presentation.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
/**
* State builder creates a specific states for SwapScreen
@ -119,11 +122,7 @@ internal class StateBuilder(val actions: UiActions) {
if (quoteModel.priceImpact > PRICE_IMPACT_THRESHOLD) {
warnings.add(SwapWarning.HighPriceImpact((quoteModel.priceImpact * HUNDRED_PERCENTS).toInt()))
}
val feeState = if (quoteModel.preparedSwapConfigState.isFeeEnough) {
FeeState.Loaded(tangemFee = quoteModel.tangemFee, fee = quoteModel.fee ?: "")
} else {
FeeState.NotEnoughFundsWarning(tangemFee = quoteModel.tangemFee, fee = quoteModel.fee ?: "")
}
val feeState = createFeeState(quoteModel, uiStateHolder)
return uiStateHolder.copy(
sendCardData = SwapCardData(
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
@ -227,38 +226,10 @@ internal class StateBuilder(val actions: UiActions) {
)
}
private fun convertPermissionState(
lastPermissionState: SwapPermissionState,
permissionDataState: PermissionDataState,
onGivePermissionClick: () -> Unit,
onChangeApproveType: (ApproveType) -> Unit,
): SwapPermissionState {
val approveType = if (lastPermissionState is SwapPermissionState.ReadyForRequest) {
lastPermissionState.approveType
} else {
ApproveType.UNLIMITED
}
return when (permissionDataState) {
PermissionDataState.Empty -> SwapPermissionState.Empty
PermissionDataState.PermissionFailed -> SwapPermissionState.Empty
PermissionDataState.PermissionLoading -> SwapPermissionState.InProgress
is PermissionDataState.PermissionReadyForRequest -> SwapPermissionState.ReadyForRequest(
currency = permissionDataState.currency,
amount = permissionDataState.amount,
approveType = approveType,
walletAddress = getShortAddressValue(permissionDataState.walletAddress),
spenderAddress = getShortAddressValue(permissionDataState.spenderAddress),
fee = permissionDataState.fee,
approveButton = ApprovePermissionButton(
enabled = true,
onClick = onGivePermissionClick,
),
cancelButton = CancelPermissionButton(
enabled = true,
),
onChangeApproveType = onChangeApproveType,
)
}
fun createSilentLoadState(uiState: SwapStateHolder): SwapStateHolder {
return uiState.copy(
updateInProgress = true,
)
}
fun updateSwapAmount(uiState: SwapStateHolder, amount: String): SwapStateHolder {
@ -284,6 +255,64 @@ internal class StateBuilder(val actions: UiActions) {
}
}
fun updateFeeSelectedItem(uiState: SwapStateHolder, item: Item<TxFee>): SwapStateHolder {
val newSelectedItem = item.copy(
startText = TextReference.Res(R.string.send_network_fee_title),
)
return when (val fee = uiState.fee) {
is FeeState.Loaded -> {
val newState = fee.state?.copy(
selectedItem = newSelectedItem,
items = selectNewItem(fee.state.items, item),
)
uiState.copy(
fee = fee.copy(state = newState),
)
}
is FeeState.NotEnoughFundsWarning -> {
val newState = fee.state?.copy(
selectedItem = newSelectedItem,
items = selectNewItem(fee.state.items, item),
)
uiState.copy(
fee = fee.copy(state = newState),
)
}
else -> uiState
}
}
private fun createFeeState(quoteModel: SwapState.QuotesLoadedState, uiStateHolder: SwapStateHolder): FeeState {
val previousFeeState = when (val stateFee = uiStateHolder.fee) {
is FeeState.Loaded -> stateFee.state
is FeeState.NotEnoughFundsWarning -> stateFee.state
else -> null
}
return if (quoteModel.preparedSwapConfigState.isFeeEnough) {
FeeState.Loaded(
tangemFee = quoteModel.tangemFee,
state = createSelectFeeState(quoteModel.swapDataModel?.fee, previousFeeState),
onSelectItem = actions.onSelectItemFee,
)
} else {
FeeState.NotEnoughFundsWarning(
tangemFee = quoteModel.tangemFee,
state = createSelectFeeState(quoteModel.swapDataModel?.fee, previousFeeState),
onSelectItem = actions.onSelectItemFee,
)
}
}
private fun selectNewItem(items: ImmutableList<Item<TxFee>>, selectItem: Item<TxFee>): ImmutableList<Item<TxFee>> {
return items.map {
if (it.id == selectItem.id) {
it.copy(isSelected = true)
} else {
it.copy(isSelected = false)
}
}.toImmutableList()
}
fun loadingPermissionState(uiState: SwapStateHolder): SwapStateHolder {
return uiState.copy(
permissionState = SwapPermissionState.InProgress,
@ -356,6 +385,103 @@ internal class StateBuilder(val actions: UiActions) {
)
}
private fun convertPermissionState(
lastPermissionState: SwapPermissionState,
permissionDataState: PermissionDataState,
onGivePermissionClick: () -> Unit,
onChangeApproveType: (ApproveType) -> Unit,
): SwapPermissionState {
val approveType = if (lastPermissionState is SwapPermissionState.ReadyForRequest) {
lastPermissionState.approveType
} else {
ApproveType.UNLIMITED
}
return when (permissionDataState) {
PermissionDataState.Empty -> SwapPermissionState.Empty
PermissionDataState.PermissionFailed -> SwapPermissionState.Empty
PermissionDataState.PermissionLoading -> SwapPermissionState.InProgress
is PermissionDataState.PermissionReadyForRequest -> SwapPermissionState.ReadyForRequest(
currency = permissionDataState.currency,
amount = permissionDataState.amount,
approveType = approveType,
walletAddress = getShortAddressValue(permissionDataState.walletAddress),
spenderAddress = getShortAddressValue(permissionDataState.spenderAddress),
fee = permissionDataState.fee,
approveButton = ApprovePermissionButton(
enabled = true,
onClick = onGivePermissionClick,
),
cancelButton = CancelPermissionButton(
enabled = true,
),
onChangeApproveType = onChangeApproveType,
)
}
}
private fun createSelectFeeState(
fee: TxFeeState?,
previousState: SelectableItemsState<TxFee>?,
): SelectableItemsState<TxFee>? {
if (fee == null) return null
if (previousState == null) {
val selectedItemId = 0
// by default preselect normal
val preselectedItem = Item(
id = selectedItemId,
startText = TextReference.Res(R.string.send_network_fee_title),
endText = TextReference.Str(fee.normalFee.feeCryptoFormatted + fee.normalFee.feeFiatFormatted),
isSelected = true,
data = fee.normalFee,
)
val feeItems = mutableListOf<Item<TxFee>>()
val normalFeeItem = Item(
id = selectedItemId,
startText = TextReference.Res(R.string.send_fee_picker_normal),
endText = TextReference.Str(fee.normalFee.feeCryptoFormatted + fee.normalFee.feeFiatFormatted),
isSelected = true,
data = fee.normalFee,
)
val priorityFeeItem = Item(
id = 1,
startText = TextReference.Res(R.string.send_fee_picker_priority),
endText = TextReference.Str(fee.priorityFee.feeCryptoFormatted + fee.priorityFee.feeFiatFormatted),
isSelected = false,
data = fee.priorityFee,
)
feeItems.add(normalFeeItem)
feeItems.add(priorityFeeItem)
return SelectableItemsState(
selectedItem = preselectedItem,
items = feeItems.toImmutableList(),
)
} else {
val normalFeeItem =
requireNotNull(previousState.items.firstOrNull()) { "in previousState there are 2 items" }
.copy(
endText = TextReference.Str(fee.normalFee.feeCryptoFormatted + fee.normalFee.feeFiatFormatted),
)
val priorityFeeItem =
requireNotNull(previousState.items.getOrNull(1)) { "in previousState there are 2 items" }
.copy(
endText = TextReference.Str(
fee.priorityFee.feeCryptoFormatted + fee.priorityFee.feeFiatFormatted,
),
)
val selectedEndText = if (normalFeeItem.isSelected) {
normalFeeItem.endText
} else {
priorityFeeItem.endText
}
return previousState.copy(
selectedItem = previousState.selectedItem.copy(
endText = selectedEndText,
),
items = listOf(normalFeeItem, priorityFeeItem).toImmutableList(),
)
}
}
private fun getShortAddressValue(fullAddress: String): String {
check(fullAddress.length > ADDRESS_MIN_LENGTH) { "Invalid address" }
val firstAddressPart = fullAddress.substring(startIndex = 0, endIndex = ADDRESS_FIRST_PART_LENGTH)
@ -366,12 +492,6 @@ internal class StateBuilder(val actions: UiActions) {
return "$firstAddressPart...$secondAddressPart"
}
fun createSilentLoadState(uiState: SwapStateHolder): SwapStateHolder {
return uiState.copy(
updateInProgress = true,
)
}
private companion object {
const val ADDRESS_MIN_LENGTH = 11
const val ADDRESS_FIRST_PART_LENGTH = 7

View file

@ -1,25 +1,10 @@
package com.tangem.feature.swap.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Card
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
@ -32,31 +17,19 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.constraintlayout.compose.ConstraintLayout
import com.tangem.core.ui.components.CardWithIcon
import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.PrimaryButtonIconRight
import com.tangem.core.ui.components.RefreshableWaringCard
import com.tangem.core.ui.components.SimpleOkDialog
import com.tangem.core.ui.components.SmallInfoCard
import com.tangem.core.ui.components.SmallInfoCardWithDisclaimer
import com.tangem.core.ui.components.SmallInfoCardWithWarning
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.WarningCard
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.components.states.Item
import com.tangem.core.ui.components.states.SelectableItemsState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.getActiveIconRes
import com.tangem.core.ui.extensions.getActiveIconResByCoinId
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.models.FeeState
import com.tangem.feature.swap.models.GenericWarningType
import com.tangem.feature.swap.models.SwapButton
import com.tangem.feature.swap.models.SwapCardData
import com.tangem.feature.swap.models.SwapPermissionState
import com.tangem.feature.swap.models.SwapStateHolder
import com.tangem.feature.swap.models.SwapWarning
import com.tangem.feature.swap.models.TransactionCardType
import com.tangem.feature.swap.domain.models.ui.TxFee
import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.presentation.R
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal
@Suppress("LongMethod")
@Composable
@ -255,12 +228,11 @@ private fun FeeItem(feeState: FeeState, currency: String) {
val disclaimer = stringResource(id = R.string.swapping_tangem_fee_disclaimer, "${feeState.tangemFee}%")
when (feeState) {
is FeeState.Loaded -> {
if (feeState.fee.isNotEmpty()) {
SmallInfoCardWithDisclaimer(
startText = titleString,
endText = feeState.fee,
if (feeState.state != null) {
SelectableInfoCard(
state = feeState.state,
disclaimer = disclaimer,
isLoading = false,
onSelect = feeState.onSelectItem,
)
}
}
@ -273,16 +245,16 @@ private fun FeeItem(feeState: FeeState, currency: String) {
)
}
is FeeState.NotEnoughFundsWarning -> {
if (feeState.fee.isNotEmpty()) {
SmallInfoCardWithWarning(
startText = titleString,
endText = feeState.fee,
disclaimer = disclaimer,
if (feeState.state != null) {
SelectableInfoCardWithWarning(
state = feeState.state,
warningText = stringResource(
id = R.string.swapping_not_enough_funds_for_fee,
currency,
currency,
),
disclaimer = disclaimer,
onSelect = feeState.onSelectItem,
)
}
}
@ -411,11 +383,56 @@ private val receiveCard = SwapCardData(
coinId = "",
)
val stateSelectable = SelectableItemsState<TxFee>(
selectedItem = Item(
0,
TextReference.Str("Balance"),
TextReference.Str("0.4405434 BTC"),
true,
TxFee(
feeValue = BigDecimal.ZERO,
gasLimit = 0,
feeFiatFormatted = "",
feeCryptoFormatted = "",
),
),
items = listOf(
Item(
0,
TextReference.Str("Normal"),
TextReference.Str("0.4405434 BTC"),
true,
TxFee(
feeValue = BigDecimal.ZERO,
gasLimit = 0,
feeFiatFormatted = "",
feeCryptoFormatted = "",
),
),
Item(
1,
TextReference.Str("Priority"),
TextReference.Str("0.46 BTC"),
false,
TxFee(
feeValue = BigDecimal.ZERO,
gasLimit = 0,
feeFiatFormatted = "",
feeCryptoFormatted = "",
),
),
).toImmutableList(),
)
private val state = SwapStateHolder(
networkId = "ethereum",
sendCardData = sendCard,
receiveCardData = receiveCard,
fee = FeeState.Loaded(fee = "0.155 MATIC (0.14 $)", tangemFee = 0.0),
fee = FeeState.Loaded(
tangemFee = 0.0,
state = stateSelectable,
onSelectItem = {},
),
warnings = listOf(SwapWarning.PermissionNeeded("DAI")),
networkCurrency = "MATIC",
swapButton = SwapButton(enabled = true, loading = false, onClick = {}),

View file

@ -3,6 +3,7 @@ package com.tangem.feature.swap.viewmodels
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData
import com.tangem.feature.swap.domain.models.ui.SwapStateData
import com.tangem.feature.swap.domain.models.ui.TxFee
data class SwapProcessDataState(
val networkId: String,
@ -11,4 +12,5 @@ data class SwapProcessDataState(
val amount: String? = null,
val approveDataModel: RequestApproveStateData? = null,
val swapDataModel: SwapStateData? = null,
val selectedFee: TxFee? = null,
)

View file

@ -228,6 +228,7 @@ internal class SwapViewModel @Inject constructor(
currencyToSend = requireNotNull(dataState.fromCurrency),
currencyToGet = requireNotNull(dataState.toCurrency),
amountToSwap = requireNotNull(dataState.amount),
fee = requireNotNull(dataState.selectedFee),
)
}
.onSuccess {
@ -436,6 +437,10 @@ internal class SwapViewModel @Inject constructor(
onChangeApproveType = { approveType ->
uiState = stateBuilder.updateApproveType(uiState, approveType)
},
onSelectItemFee = { feeItem ->
dataState = dataState.copy(selectedFee = feeItem.data)
uiState = stateBuilder.updateFeeSelectedItem(uiState, feeItem)
},
)
}

View file

@ -1,7 +1,7 @@
package com.tangem.lib.crypto
import com.tangem.lib.crypto.models.Currency
import com.tangem.lib.crypto.models.ProxyFee
import com.tangem.lib.crypto.models.ProxyFees
import com.tangem.lib.crypto.models.ProxyNetworkInfo
import com.tangem.lib.crypto.models.transactions.SendTxResult
import java.math.BigDecimal
@ -55,7 +55,7 @@ interface TransactionManager {
increaseBy: Int?,
data: String?,
derivationPath: String?,
): ProxyFee
): ProxyFees
@Throws(IllegalStateException::class)
fun getNativeTokenDecimals(networkId: String): Int

View file

@ -0,0 +1,7 @@
package com.tangem.lib.crypto.models
data class ProxyFees(
val minFee: ProxyFee,
val normalFee: ProxyFee,
val priorityFee: ProxyFee,
)