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">
|
<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>
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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%
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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"))
|
||||||
}
|
}
|
||||||
|
|
@ -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 = {},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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 = "∞"
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 = {}),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
@ -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)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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