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"> <code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="120" /> <option name="RIGHT_MARGIN" value="120" />
<option name="SOFT_MARGINS" 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> <JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS"> <option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value> <value>

View file

@ -67,6 +67,7 @@ sealed class CardInfo(
) )
} }
// TODO("Remove and use the same from coreUI")
sealed interface TextReference { sealed interface TextReference {
class Res(@StringRes val id: Int, val formatArgs: List<Any> = emptyList()) : TextReference { class Res(@StringRes val id: Int, val formatArgs: List<Any> = emptyList()) : TextReference {
constructor(@StringRes id: Int, vararg formatArgs: Any) : this(id, formatArgs.toList()) 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.EthereumTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.blockchains.optimism.OptimismWalletManager import com.tangem.blockchain.blockchains.optimism.OptimismWalletManager
import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.*
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.extensions.Result import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.blockchain.extensions.isNetworkError 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.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.models.Currency import com.tangem.lib.crypto.models.*
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.transactions.SendTxResult import com.tangem.lib.crypto.models.transactions.SendTxResult
import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.Basic
@ -152,7 +142,7 @@ class TransactionManagerImpl(
increaseBy: Int?, increaseBy: Int?,
data: String?, data: String?,
derivationPath: String?, derivationPath: String?,
): ProxyFee { ): ProxyFees {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain, derivationPath) val walletManager = getActualWalletManager(blockchain, derivationPath)
if (walletManager is EthereumWalletManager) { if (walletManager is EthereumWalletManager) {
@ -199,7 +189,7 @@ class TransactionManagerImpl(
currency: Currency, currency: Currency,
blockchain: Blockchain, blockchain: Blockchain,
destinationAddress: String, destinationAddress: String,
): ProxyFee { ): ProxyFees {
val fee = (walletManager as? TransactionSender)?.getFee( val fee = (walletManager as? TransactionSender)?.getFee(
amount = createAmount(amountToSend, currency, blockchain), amount = createAmount(amountToSend, currency, blockchain),
destination = destinationAddress, destination = destinationAddress,
@ -207,9 +197,23 @@ class TransactionManagerImpl(
return when (fee) { return when (fee) {
is Result.Success -> { is Result.Success -> {
// for not EVM blockchains set gasLimit ZERO for now // 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, 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 -> { is Result.Failure -> {
@ -227,7 +231,7 @@ class TransactionManagerImpl(
destinationAddress: String, destinationAddress: String,
data: String?, data: String?,
increaseBy: Int?, increaseBy: Int?,
): ProxyFee { ): ProxyFees {
val gasLimit = getGasLimit( val gasLimit = getGasLimit(
evmWalletManager = walletManager, evmWalletManager = walletManager,
blockchain = blockchain, blockchain = blockchain,
@ -235,27 +239,10 @@ class TransactionManagerImpl(
currency = currency, currency = currency,
destinationAddress = destinationAddress, destinationAddress = destinationAddress,
data = data, data = data,
).let { ).increaseBigIntegerByPercents(increaseBy)
if (increaseBy != null && increaseBy != 0) {
it.multiply(increaseBy.toBigInteger()).divide(BigInteger("100"))
} else {
it
}
}
return when (val gasPrice = walletManager.getGasPrice()) { return when (val gasPrice = walletManager.getGasPrice()) {
is Result.Success -> { is Result.Success -> {
val fee = gasLimit.multiply(gasPrice.data).toBigDecimal( createProxyFees(gasPrice = gasPrice.data, gasLimit = gasLimit, blockchain = blockchain)
scale = blockchain.decimals(),
mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN),
)
ProxyFee(
gasLimit = gasLimit,
fee = ProxyAmount(
currencySymbol = blockchain.currency,
value = fee,
decimals = blockchain.decimals(),
),
)
} }
is Result.Failure -> { is Result.Failure -> {
error(gasPrice.error.message ?: gasPrice.error.customMessage) error(gasPrice.error.message ?: gasPrice.error.customMessage)
@ -268,17 +255,31 @@ class TransactionManagerImpl(
amount: Amount, amount: Amount,
destinationAddress: String, destinationAddress: String,
data: String?, data: String?,
): ProxyFee { ): ProxyFees {
val fee = if (data.isNullOrEmpty()) { val fee = if (data.isNullOrEmpty()) {
walletManager.getFee(amount, destinationAddress) walletManager.getFee(amount, destinationAddress)
} else { } else {
walletManager.getFee(amount, destinationAddress, data) walletManager.getFee(amount, destinationAddress, data)
} }
when (fee) { return when (fee) {
is Result.Success -> { is Result.Success -> {
return ProxyFee( val minFee = fee.data.firstOrNull() ?: error("no fee found")
val minProxyFee = ProxyFee(
gasLimit = walletManager.gasLimit ?: BigInteger.ZERO, 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 -> { 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 { private fun createAmount(amount: BigDecimal, currency: Currency, blockchain: Blockchain): Amount {
return when (currency) { return when (currency) {
is Currency.NativeToken -> { 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 { companion object {
private const val HEX_PREFIX = "0x" private const val HEX_PREFIX = "0x"
private const val USER_CANCELLED_ERROR_CODE = 50002 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.compose.accompanist.systemUiController)
implementation(deps.material) implementation(deps.material)
implementation(deps.compose.shimmer) implementation(deps.compose.shimmer)
implementation(deps.kotlin.immutable.collections)
implementation(project(":core:res")) 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.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
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.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Divider import androidx.compose.material.Divider
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.R 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.tangem.core.ui.res.TangemTheme
import com.valentinilk.shimmer.shimmer import com.valentinilk.shimmer.shimmer
import kotlinx.collections.immutable.toImmutableList
/** /**
* Small card with text information attached to the edges * 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), modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12),
) )
Row( WarningItem(warningText = warningText)
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,
)
}
} }
} }
} }
@ -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 // region elements
@Composable @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 @Composable
private fun ShimmerItem(width: Dp, height: Dp) { private fun ShimmerItem(width: Dp, height: Dp) {
Box( 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 // endregion elements
// region Preview // 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 currencyToSend [Currency]
* @param currencyToGet [Currency] * @param currencyToGet [Currency]
* @param amountToSwap amount to swap * @param amountToSwap amount to swap
* @param fee for tx
* @return [TxState] * @return [TxState]
*/ */
@Suppress("LongParameterList")
@Throws(IllegalStateException::class) @Throws(IllegalStateException::class)
suspend fun onSwap( suspend fun onSwap(
networkId: String, networkId: String,
@ -89,6 +91,7 @@ interface SwapInteractor {
currencyToSend: Currency, currencyToSend: Currency,
currencyToGet: Currency, currencyToGet: Currency,
amountToSwap: String, amountToSwap: String,
fee: TxFee,
): TxState ): 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.feature.swap.domain.models.ui.*
import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.UserWalletManager 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.ProxyFiatCurrency
import com.tangem.lib.crypto.models.transactions.SendTxResult import com.tangem.lib.crypto.models.transactions.SendTxResult
import com.tangem.utils.toFiatString import com.tangem.utils.toFiatString
@ -188,14 +189,15 @@ internal class SwapInteractorImpl @Inject constructor(
currencyToSend: Currency, currencyToSend: Currency,
currencyToGet: Currency, currencyToGet: Currency,
amountToSwap: String, amountToSwap: String,
fee: TxFee,
): TxState { ): TxState {
val amount = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format, use only digits" } val amount = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format, use only digits" }
val result = transactionManager.sendTransaction( val result = transactionManager.sendTransaction(
networkId = networkId, networkId = networkId,
amountToSend = amount, amountToSend = amount,
currencyToSend = cryptoCurrencyConverter.convert(currencyToSend), currencyToSend = cryptoCurrencyConverter.convert(currencyToSend),
feeAmount = swapStateData.fee, feeAmount = fee.feeValue,
gasLimit = swapStateData.gasLimit, gasLimit = fee.gasLimit,
destinationAddress = swapStateData.swapModel.transaction.toWalletAddress, destinationAddress = swapStateData.swapModel.transaction.toWalletAddress,
dataToSign = swapStateData.swapModel.transaction.data, dataToSign = swapStateData.swapModel.transaction.data,
isSwap = true, isSwap = true,
@ -346,7 +348,6 @@ internal class SwapInteractorImpl @Inject constructor(
fromTokenAmount = quoteDataModel.fromTokenAmount, fromTokenAmount = quoteDataModel.fromTokenAmount,
toTokenAmount = quoteDataModel.toTokenAmount, toTokenAmount = quoteDataModel.toTokenAmount,
swapStateData = null, swapStateData = null,
formattedFee = null,
) )
val quotesState = updatePermissionState( val quotesState = updatePermissionState(
networkId = networkId, 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 appCurrency = userWalletManager.getUserAppCurrency()
val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId) val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId)
val rates = repository.getRates(appCurrency.code, listOf(nativeToken.id)) val rates = repository.getRates(appCurrency.code, listOf(nativeToken.id))
return rates[nativeToken.id]?.toBigDecimal()?.let { rate -> return rates[nativeToken.id]?.toBigDecimal()?.let { rate ->
" (${fee.toFiatString(rate, appCurrency.symbol, true)})" fees.map { fee ->
" (${fee.toFiatString(rate, appCurrency.symbol, true)})"
}
}.orEmpty() }.orEmpty()
} }
@ -406,15 +409,11 @@ internal class SwapInteractorImpl @Inject constructor(
data = swapData.transaction.data, data = swapData.transaction.data,
derivationPath = derivationPath, derivationPath = derivationPath,
) )
val feeFiat = getFormattedFiatFee(networkId, feeData.fee.value) val txFeeState = proxyFeesToFeeState(networkId, feeData)
val formattedFee = amountFormatter.formatBigDecimalAmountToUI( val isBalanceIncludeFeeEnough =
amount = feeData.fee.value, isBalanceEnough(networkId, fromToken, amount, txFeeState.priorityFee.feeValue)
decimals = transactionManager.getNativeTokenDecimals(networkId),
currency = userWalletManager.getNetworkCurrency(networkId),
) + feeFiat
val isBalanceIncludeFeeEnough = isBalanceEnough(networkId, fromToken, amount, feeData.fee.value)
val isFeeEnough = checkFeeIsEnough( val isFeeEnough = checkFeeIsEnough(
fee = feeData.fee.value, fee = txFeeState.priorityFee.feeValue,
spendAmount = amount, spendAmount = amount,
networkId = networkId, networkId = networkId,
fromToken = fromToken, fromToken = fromToken,
@ -425,10 +424,8 @@ internal class SwapInteractorImpl @Inject constructor(
toToken = toToken, toToken = toToken,
fromTokenAmount = swapData.fromTokenAmount, fromTokenAmount = swapData.fromTokenAmount,
toTokenAmount = swapData.toTokenAmount, toTokenAmount = swapData.toTokenAmount,
formattedFee = formattedFee,
swapStateData = SwapStateData( swapStateData = SwapStateData(
gasLimit = feeData.gasLimit.toInt(), fee = txFeeState,
fee = feeData.fee.value,
swapModel = swapData, swapModel = swapData,
), ),
) )
@ -453,7 +450,6 @@ internal class SwapInteractorImpl @Inject constructor(
toToken: Currency, toToken: Currency,
fromTokenAmount: SwapAmount, fromTokenAmount: SwapAmount,
toTokenAmount: SwapAmount, toTokenAmount: SwapAmount,
formattedFee: String?,
swapStateData: SwapStateData?, swapStateData: SwapStateData?,
): SwapState.QuotesLoadedState { ): SwapState.QuotesLoadedState {
val appCurrency = userWalletManager.getUserAppCurrency() val appCurrency = userWalletManager.getUserAppCurrency()
@ -484,7 +480,6 @@ internal class SwapInteractorImpl @Inject constructor(
formatWithSpaces = true, formatWithSpaces = true,
), ),
), ),
fee = formattedFee,
priceImpact = calculatePriceImpact( priceImpact = calculatePriceImpact(
fromTokenAmount = fromTokenAmount.value, fromTokenAmount = fromTokenAmount.value,
fromRate = rates[fromToken.id] ?: 0.0, fromRate = rates[fromToken.id] ?: 0.0,
@ -531,20 +526,19 @@ internal class SwapInteractorImpl @Inject constructor(
data = transactionData.data, data = transactionData.data,
derivationPath = derivationPath, derivationPath = derivationPath,
) )
val feeFiat = getFormattedFiatFee(networkId, feeData.fee.value) val feeFiat = getFormattedFiatFees(networkId, feeData.normalFee.fee.value)
val formattedFee = amountFormatter.formatBigDecimalAmountToUI( val formattedFee = amountFormatter.formatBigDecimalAmountToUI(
amount = feeData.fee.value, amount = feeData.normalFee.fee.value,
decimals = transactionManager.getNativeTokenDecimals(networkId), decimals = transactionManager.getNativeTokenDecimals(networkId),
currency = userWalletManager.getNetworkCurrency(networkId), currency = userWalletManager.getNetworkCurrency(networkId),
) + feeFiat ) + (feeFiat.firstOrNull() ?: "")
val isFeeEnough = checkFeeIsEnough( val isFeeEnough = checkFeeIsEnough(
fee = feeData.fee.value, fee = feeData.normalFee.fee.value,
spendAmount = SwapAmount.zeroSwapAmount(), spendAmount = SwapAmount.zeroSwapAmount(),
networkId = networkId, networkId = networkId,
fromToken = fromToken, fromToken = fromToken,
) )
return quotesLoadedState.copy( return quotesLoadedState.copy(
fee = formattedFee,
permissionState = PermissionDataState.PermissionReadyForRequest( permissionState = PermissionDataState.PermissionReadyForRequest(
currency = fromToken.symbol, currency = fromToken.symbol,
amount = INFINITY_SYMBOL, amount = INFINITY_SYMBOL,
@ -552,8 +546,8 @@ internal class SwapInteractorImpl @Inject constructor(
spenderAddress = transactionData.toAddress, spenderAddress = transactionData.toAddress,
fee = formattedFee, fee = formattedFee,
requestApproveData = RequestApproveStateData( requestApproveData = RequestApproveStateData(
fee = feeData.fee.value, fee = feeData.normalFee.fee.value,
gasLimit = feeData.gasLimit.toInt(), gasLimit = feeData.normalFee.gasLimit.toInt(),
approveModel = transactionData, 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( private fun isBalanceEnough(
networkId: String, networkId: String,
fromToken: Currency, fromToken: Currency,
@ -654,7 +680,7 @@ internal class SwapInteractorImpl @Inject constructor(
private const val ZERO_BALANCE = "0" private const val ZERO_BALANCE = "0"
private const val DEFAULT_BLOCKCHAIN_INCH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" private const val DEFAULT_BLOCKCHAIN_INCH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
private const val INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT = 1.4 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 USDT_SYMBOL = "USDT"
private const val USDC_SYMBOL = "USDC" private const val USDC_SYMBOL = "USDC"
private const val INFINITY_SYMBOL = "" private const val INFINITY_SYMBOL = ""

View file

@ -12,7 +12,6 @@ sealed interface SwapState {
data class QuotesLoadedState( data class QuotesLoadedState(
val fromTokenInfo: TokenSwapInfo, val fromTokenInfo: TokenSwapInfo,
val toTokenInfo: TokenSwapInfo, val toTokenInfo: TokenSwapInfo,
val fee: String?,
val priceImpact: Float, val priceImpact: Float,
val networkCurrency: String, val networkCurrency: String,
val preparedSwapConfigState: PreparedSwapConfigState = PreparedSwapConfigState( val preparedSwapConfigState: PreparedSwapConfigState = PreparedSwapConfigState(
@ -66,7 +65,18 @@ data class RequestApproveStateData(
) )
data class SwapStateData( data class SwapStateData(
val fee: BigDecimal, val fee: TxFeeState,
val gasLimit: Int,
val swapModel: SwapDataModel, 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 package com.tangem.feature.swap.models
import androidx.compose.ui.text.input.TextFieldValue 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( data class SwapStateHolder(
val sendCardData: SwapCardData, val sendCardData: SwapCardData,
@ -54,12 +57,17 @@ sealed class FeeState(open val tangemFee: Double) {
data class Loaded( data class Loaded(
override val tangemFee: Double, override val tangemFee: Double,
val fee: String = "", val state: SelectableItemsState<TxFee>?,
val onSelectItem: (Item<TxFee>) -> Unit,
) : FeeState(tangemFee) ) : FeeState(tangemFee)
object Loading : FeeState(0.0) 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 { sealed interface TransactionCardType {

View file

@ -1,5 +1,8 @@
package com.tangem.feature.swap.models 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( data class UiActions(
val onSearchEntered: (String) -> Unit, val onSearchEntered: (String) -> Unit,
val onSearchFocusChange: (Boolean) -> Unit, val onSearchFocusChange: (Boolean) -> Unit,
@ -14,4 +17,5 @@ data class UiActions(
val openPermissionBottomSheet: () -> Unit, val openPermissionBottomSheet: () -> Unit,
val hidePermissionBottomSheet: () -> Unit, val hidePermissionBottomSheet: () -> Unit,
val onChangeApproveType: (ApproveType) -> 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.TextRange
import androidx.compose.ui.text.input.TextFieldValue 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.converters.TokensDataConverter
import com.tangem.feature.swap.domain.models.DataError 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.Currency
import com.tangem.feature.swap.domain.models.domain.NetworkInfo 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.domain.isNonNative
import com.tangem.feature.swap.domain.models.formatToUIRepresentation 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.*
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.models.* 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 * State builder creates a specific states for SwapScreen
@ -119,11 +122,7 @@ internal class StateBuilder(val actions: UiActions) {
if (quoteModel.priceImpact > PRICE_IMPACT_THRESHOLD) { if (quoteModel.priceImpact > PRICE_IMPACT_THRESHOLD) {
warnings.add(SwapWarning.HighPriceImpact((quoteModel.priceImpact * HUNDRED_PERCENTS).toInt())) warnings.add(SwapWarning.HighPriceImpact((quoteModel.priceImpact * HUNDRED_PERCENTS).toInt()))
} }
val feeState = if (quoteModel.preparedSwapConfigState.isFeeEnough) { val feeState = createFeeState(quoteModel, uiStateHolder)
FeeState.Loaded(tangemFee = quoteModel.tangemFee, fee = quoteModel.fee ?: "")
} else {
FeeState.NotEnoughFundsWarning(tangemFee = quoteModel.tangemFee, fee = quoteModel.fee ?: "")
}
return uiStateHolder.copy( return uiStateHolder.copy(
sendCardData = SwapCardData( sendCardData = SwapCardData(
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard), type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
@ -227,38 +226,10 @@ internal class StateBuilder(val actions: UiActions) {
) )
} }
private fun convertPermissionState( fun createSilentLoadState(uiState: SwapStateHolder): SwapStateHolder {
lastPermissionState: SwapPermissionState, return uiState.copy(
permissionDataState: PermissionDataState, updateInProgress = true,
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 updateSwapAmount(uiState: SwapStateHolder, amount: String): SwapStateHolder { 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 { fun loadingPermissionState(uiState: SwapStateHolder): SwapStateHolder {
return uiState.copy( return uiState.copy(
permissionState = SwapPermissionState.InProgress, 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 { private fun getShortAddressValue(fullAddress: String): String {
check(fullAddress.length > ADDRESS_MIN_LENGTH) { "Invalid address" } check(fullAddress.length > ADDRESS_MIN_LENGTH) { "Invalid address" }
val firstAddressPart = fullAddress.substring(startIndex = 0, endIndex = ADDRESS_FIRST_PART_LENGTH) val firstAddressPart = fullAddress.substring(startIndex = 0, endIndex = ADDRESS_FIRST_PART_LENGTH)
@ -366,12 +492,6 @@ internal class StateBuilder(val actions: UiActions) {
return "$firstAddressPart...$secondAddressPart" return "$firstAddressPart...$secondAddressPart"
} }
fun createSilentLoadState(uiState: SwapStateHolder): SwapStateHolder {
return uiState.copy(
updateInProgress = true,
)
}
private companion object { private companion object {
const val ADDRESS_MIN_LENGTH = 11 const val ADDRESS_MIN_LENGTH = 11
const val ADDRESS_FIRST_PART_LENGTH = 7 const val ADDRESS_FIRST_PART_LENGTH = 7

View file

@ -1,25 +1,10 @@
package com.tangem.feature.swap.ui package com.tangem.feature.swap.ui
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image import androidx.compose.foundation.*
import androidx.compose.foundation.background import androidx.compose.foundation.layout.*
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.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll import androidx.compose.material.*
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.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember 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.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.ConstraintLayout
import com.tangem.core.ui.components.CardWithIcon import com.tangem.core.ui.components.*
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.appbar.AppBarWithBackButton 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.getActiveIconRes
import com.tangem.core.ui.extensions.getActiveIconResByCoinId import com.tangem.core.ui.extensions.getActiveIconResByCoinId
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.models.FeeState import com.tangem.feature.swap.domain.models.ui.TxFee
import com.tangem.feature.swap.models.GenericWarningType import com.tangem.feature.swap.models.*
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.presentation.R import com.tangem.feature.swap.presentation.R
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal
@Suppress("LongMethod") @Suppress("LongMethod")
@Composable @Composable
@ -255,12 +228,11 @@ private fun FeeItem(feeState: FeeState, currency: String) {
val disclaimer = stringResource(id = R.string.swapping_tangem_fee_disclaimer, "${feeState.tangemFee}%") val disclaimer = stringResource(id = R.string.swapping_tangem_fee_disclaimer, "${feeState.tangemFee}%")
when (feeState) { when (feeState) {
is FeeState.Loaded -> { is FeeState.Loaded -> {
if (feeState.fee.isNotEmpty()) { if (feeState.state != null) {
SmallInfoCardWithDisclaimer( SelectableInfoCard(
startText = titleString, state = feeState.state,
endText = feeState.fee,
disclaimer = disclaimer, disclaimer = disclaimer,
isLoading = false, onSelect = feeState.onSelectItem,
) )
} }
} }
@ -273,16 +245,16 @@ private fun FeeItem(feeState: FeeState, currency: String) {
) )
} }
is FeeState.NotEnoughFundsWarning -> { is FeeState.NotEnoughFundsWarning -> {
if (feeState.fee.isNotEmpty()) { if (feeState.state != null) {
SmallInfoCardWithWarning( SelectableInfoCardWithWarning(
startText = titleString, state = feeState.state,
endText = feeState.fee,
disclaimer = disclaimer,
warningText = stringResource( warningText = stringResource(
id = R.string.swapping_not_enough_funds_for_fee, id = R.string.swapping_not_enough_funds_for_fee,
currency, currency,
currency, currency,
), ),
disclaimer = disclaimer,
onSelect = feeState.onSelectItem,
) )
} }
} }
@ -411,11 +383,56 @@ private val receiveCard = SwapCardData(
coinId = "", 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( private val state = SwapStateHolder(
networkId = "ethereum", networkId = "ethereum",
sendCardData = sendCard, sendCardData = sendCard,
receiveCardData = receiveCard, 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")), warnings = listOf(SwapWarning.PermissionNeeded("DAI")),
networkCurrency = "MATIC", networkCurrency = "MATIC",
swapButton = SwapButton(enabled = true, loading = false, onClick = {}), 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.domain.Currency
import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData 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.SwapStateData
import com.tangem.feature.swap.domain.models.ui.TxFee
data class SwapProcessDataState( data class SwapProcessDataState(
val networkId: String, val networkId: String,
@ -11,4 +12,5 @@ data class SwapProcessDataState(
val amount: String? = null, val amount: String? = null,
val approveDataModel: RequestApproveStateData? = null, val approveDataModel: RequestApproveStateData? = null,
val swapDataModel: SwapStateData? = null, val swapDataModel: SwapStateData? = null,
val selectedFee: TxFee? = null,
) )

View file

@ -228,6 +228,7 @@ internal class SwapViewModel @Inject constructor(
currencyToSend = requireNotNull(dataState.fromCurrency), currencyToSend = requireNotNull(dataState.fromCurrency),
currencyToGet = requireNotNull(dataState.toCurrency), currencyToGet = requireNotNull(dataState.toCurrency),
amountToSwap = requireNotNull(dataState.amount), amountToSwap = requireNotNull(dataState.amount),
fee = requireNotNull(dataState.selectedFee),
) )
} }
.onSuccess { .onSuccess {
@ -436,6 +437,10 @@ internal class SwapViewModel @Inject constructor(
onChangeApproveType = { approveType -> onChangeApproveType = { approveType ->
uiState = stateBuilder.updateApproveType(uiState, 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 package com.tangem.lib.crypto
import com.tangem.lib.crypto.models.Currency 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.ProxyNetworkInfo
import com.tangem.lib.crypto.models.transactions.SendTxResult import com.tangem.lib.crypto.models.transactions.SendTxResult
import java.math.BigDecimal import java.math.BigDecimal
@ -55,7 +55,7 @@ interface TransactionManager {
increaseBy: Int?, increaseBy: Int?,
data: String?, data: String?,
derivationPath: String?, derivationPath: String?,
): ProxyFee ): ProxyFees
@Throws(IllegalStateException::class) @Throws(IllegalStateException::class)
fun getNativeTokenDecimals(networkId: String): Int 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,
)