Updated on 2026-08-14
This commit is contained in:
parent
c1f4cb7388
commit
9c762583a0
18 changed files with 759 additions and 217 deletions
4
.idea/codeStyles/Project.xml
generated
4
.idea/codeStyles/Project.xml
generated
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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%
|
||||
}
|
||||
}
|
||||
|
|
@ -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"))
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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 = "∞"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {}),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.lib.crypto.models
|
||||
|
||||
data class ProxyFees(
|
||||
val minFee: ProxyFee,
|
||||
val normalFee: ProxyFee,
|
||||
val priorityFee: ProxyFee,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue