Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-27 00:35:31 +03:00
parent 3e5ab44b4c
commit 9505a1404b
26 changed files with 576 additions and 542 deletions

View file

@ -224,20 +224,18 @@ class TransactionManagerImpl(
// for not EVM blockchains set gasLimit ZERO for now
when (fee.data) {
is TransactionFee.Single -> {
val fee = (fee.data as TransactionFee.Single).normal
val normalFee = (fee.data as TransactionFee.Single).normal
val singleFee = ProxyFee(
gasLimit = BigInteger.ZERO,
fee = convertToProxyAmount(amount = fee.amount),
fee = convertToProxyAmount(amount = normalFee.amount),
)
ProxyFees(
minFee = singleFee,
normalFee = singleFee,
priorityFee = singleFee,
ProxyFees.SingleFee(
singleFee = singleFee,
)
}
is TransactionFee.Choosable -> {
val choosableFee = fee.data as TransactionFee.Choosable
ProxyFees(
ProxyFees.MultipleFees(
minFee = ProxyFee(
gasLimit = BigInteger.ZERO,
fee = convertToProxyAmount(amount = choosableFee.minimum.amount),
@ -280,7 +278,7 @@ class TransactionManagerImpl(
).increaseBigIntegerByPercents(increaseBy)
return when (val gasPrice = walletManager.getGasPrice()) {
is Result.Success -> {
createProxyFees(gasPrice = gasPrice.data, gasLimit = gasLimit, blockchain = blockchain)
createMultipleProxyFees(gasPrice = gasPrice.data, gasLimit = gasLimit, blockchain = blockchain)
}
is Result.Failure -> {
error(gasPrice.error.message ?: gasPrice.error.customMessage)
@ -316,7 +314,7 @@ class TransactionManagerImpl(
fee = convertToProxyAmount(amount = choosableFee.priority.amount),
)
ProxyFees(
ProxyFees.MultipleFees(
minFee = minProxyFee,
normalFee = normalProxyFee,
priorityFee = priorityProxyFee,
@ -456,7 +454,7 @@ class TransactionManagerImpl(
* @param gasLimit
* @param blockchain
*/
private fun createProxyFees(gasPrice: BigInteger, gasLimit: BigInteger, blockchain: Blockchain): ProxyFees {
private fun createMultipleProxyFees(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(
@ -495,7 +493,7 @@ class TransactionManagerImpl(
decimals = blockchain.decimals(),
),
)
return ProxyFees(
return ProxyFees.MultipleFees(
minFee = minFee,
normalFee = normalFee,
priorityFee = priorityFee,

View file

@ -8,7 +8,6 @@ import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query
import java.math.BigDecimal
/**
* Interface of Tangem Express API (new swap mechanism)

View file

@ -19,7 +19,7 @@ import com.tangem.core.ui.res.TangemTheme
* https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-807&mode=design&t=Ygv5sohTTHYAQcBS-4
*/
@Composable
fun SimpleActionRow(title: String, description: String, modifier: Modifier = Modifier) {
fun SimpleActionRow(title: String, description: String, modifier: Modifier = Modifier, isClickable: Boolean = true) {
Box(
modifier = modifier
.background(color = TangemTheme.colors.background.action)
@ -44,14 +44,16 @@ fun SimpleActionRow(title: String, description: String, modifier: Modifier = Mod
)
}
Icon(
painter = painterResource(id = R.drawable.ic_chevron_right_24),
contentDescription = null,
modifier = Modifier
.align(alignment = Alignment.CenterEnd)
.padding(end = TangemTheme.dimens.spacing12),
tint = TangemTheme.colors.icon.informative,
)
if (isClickable) {
Icon(
painter = painterResource(id = R.drawable.ic_chevron_right_24),
contentDescription = null,
modifier = Modifier
.align(alignment = Alignment.CenterEnd)
.padding(end = TangemTheme.dimens.spacing12),
tint = TangemTheme.colors.icon.informative,
)
}
}
}

View file

@ -25,7 +25,7 @@ fun BigDecimal.toFormattedString(
@Suppress("MagicNumber")
fun BigDecimal.toFormattedCurrencyString(
decimals: Int,
currency: String,
currency: String? = null,
roundingMode: RoundingMode = RoundingMode.DOWN,
limitNumberOfDecimals: Boolean = true,
): String {
@ -38,7 +38,8 @@ fun BigDecimal.toFormattedCurrencyString(
decimals = decimalsForRounding,
roundingMode = roundingMode,
)
return "$formattedAmount $currency"
val formattedCurrency = currency?.let { " $it " } ?: ""
return "$formattedAmount$formattedCurrency"
}
fun BigDecimal.toFiatString(

View file

@ -13,8 +13,6 @@ import com.tangem.datasource.api.express.models.response.SwapPair
import com.tangem.datasource.api.express.models.response.SwapPairsWithProviders
import com.tangem.datasource.api.oneinch.OneInchApi
import com.tangem.datasource.api.oneinch.OneInchApiFactory
import com.tangem.datasource.api.oneinch.OneInchErrorsHandler
import com.tangem.datasource.api.oneinch.errors.OneIncResponseException
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.common.extensions.fromNetworkId
@ -41,7 +39,6 @@ internal class SwapRepositoryImpl @Inject constructor(
private val tangemTechApi: TangemTechApi,
private val tangemExpressApi: TangemExpressApi,
private val oneInchApiFactory: OneInchApiFactory,
private val oneInchErrorsHandler: OneInchErrorsHandler,
private val coroutineDispatcher: CoroutineDispatcherProvider,
private val configManager: ConfigManager,
private val walletManagersFacade: WalletManagersFacade,
@ -203,7 +200,7 @@ internal class SwapRepositoryImpl @Inject constructor(
toAddress = toAddress,
).getOrThrow()
AggregatedSwapDataModel(
dataModel = expressDataConverter.convert(response)
dataModel = expressDataConverter.convert(response),
)
} catch (ex: Exception) {
AggregatedSwapDataModel(null, mapErrors(ex.message))

View file

@ -24,7 +24,7 @@ class ExpressDataConverter : Converter<ExchangeDataResponse, SwapDataModel> {
txId = transactionDto.txId,
txTo = transactionDto.txTo,
txFrom = requireNotNull(transactionDto.txFrom),
txData = requireNotNull(transactionDto.txData)
txData = requireNotNull(transactionDto.txData),
)
} else {
ExpressTransactionModel.CEX(
@ -37,5 +37,4 @@ class ExpressDataConverter : Converter<ExchangeDataResponse, SwapDataModel> {
)
}
}
}

View file

@ -2,7 +2,6 @@ package com.tangem.feature.swap.di
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.oneinch.OneInchApiFactory
import com.tangem.datasource.api.oneinch.OneInchErrorsHandler
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -26,7 +25,6 @@ class SwapDataModule {
tangemTechApi: TangemTechApi,
tangemExpressApi: TangemExpressApi,
oneInchApiFactory: OneInchApiFactory,
oneInchErrorsHandler: OneInchErrorsHandler,
coroutineDispatcher: CoroutineDispatcherProvider,
configManager: ConfigManager,
walletManagerFacade: WalletManagersFacade,
@ -36,7 +34,6 @@ class SwapDataModule {
tangemTechApi = tangemTechApi,
tangemExpressApi = tangemExpressApi,
oneInchApiFactory = oneInchApiFactory,
oneInchErrorsHandler = oneInchErrorsHandler,
coroutineDispatcher = coroutineDispatcher,
configManager = configManager,
walletManagersFacade = walletManagerFacade,

View file

@ -23,6 +23,8 @@ dependencies {
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.transaction)
implementation(projects.domain.legacy)
/** Core modules */
implementation(projects.core.utils)
@ -35,4 +37,5 @@ dependencies {
implementation(deps.kotlin.coroutines)
implementation(deps.arrow.core)
implementation(deps.timber)
implementation(deps.tangem.blockchain)
}

View file

@ -7,7 +7,6 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.swap.domain.models.ui.*
import java.math.BigDecimal
interface SwapInteractor {
@ -71,13 +70,21 @@ interface SwapInteractor {
suspend fun onSwap(
exchangeProviderType: ExchangeProviderType,
networkId: String,
swapStateData: SwapStateData,
swapData: SwapDataModel,
currencyToSend: CryptoCurrency,
currencyToGet: CryptoCurrency,
amountToSwap: String,
fee: TxFee,
): TxState
suspend fun updateQuotesStateWithSelectedFee(
state: SwapState.QuotesLoadedState,
selectedFee: FeeType,
fromToken: CryptoCurrencyStatus,
amountToSwap: String,
networkId: String,
): SwapState.QuotesLoadedState
/**
* Returns token in wallet balance
*
@ -88,14 +95,5 @@ interface SwapInteractor {
fun isAvailableToSwap(networkId: String): Boolean
fun getSwapAmountForToken(amount: String, token: CryptoCurrency): SwapAmount
suspend fun checkFeeIsEnough(
fee: BigDecimal?,
spendAmount: SwapAmount,
networkId: String,
fromToken: CryptoCurrency,
): Boolean
fun getSelectedWallet(): UserWallet?
}

View file

@ -1,6 +1,8 @@
package com.tangem.feature.swap.domain
import arrow.core.getOrElse
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
@ -10,6 +12,8 @@ import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.feature.swap.domain.cache.SwapDataCache
@ -24,10 +28,10 @@ import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.lib.crypto.models.*
import com.tangem.lib.crypto.models.transactions.SendTxResult
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.toFiatString
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.firstOrNull
import timber.log.Timber
import java.math.BigDecimal
import java.math.RoundingMode
@ -45,7 +49,9 @@ internal class SwapInteractorImpl @Inject constructor(
private val walletFeatureToggles: WalletFeatureToggles,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase,
private val walletManagersFacade: WalletManagersFacade,
private val quotesRepository: QuotesRepository,
private val dispatcher: CoroutineDispatcherProvider,
) : SwapInteractor {
// TODO: Move to DI
@ -53,6 +59,10 @@ internal class SwapInteractorImpl @Inject constructor(
AddCryptoCurrenciesUseCase(currenciesRepository, networksRepository)
}
private val getFeeUseCase by lazy(LazyThreadSafetyMode.NONE) {
GetFeeUseCase(walletManagersFacade, dispatcher)
}
private val swapCurrencyConverter = SwapCurrencyConverter()
private val amountFormatter = AmountFormatter()
private var derivationPath: String? = null
@ -236,18 +246,11 @@ internal class SwapInteractorImpl @Inject constructor(
createEmptyAmountState(
networkId,
fromToken.currency,
toToken.currency
toToken.currency,
)
}
}
val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken.currency))
val fromTokenAddress = getTokenAddress(fromToken.currency)
val toTokenAddress = getTokenAddress(toToken.currency)
val isAllowedToSpend = isAllowedToSpend(networkId, fromToken.currency, amount)
if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) {
allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress)
transactionManager.updateWalletManager(networkId, derivationPath)
}
val isBalanceWithoutFeeEnough = isBalanceEnough(networkId, fromToken.currency, amount, null)
when (provider.type) {
@ -256,12 +259,9 @@ internal class SwapInteractorImpl @Inject constructor(
networkId = networkId,
fromToken = fromToken,
toToken = toToken,
fromTokenAddress = fromTokenAddress,
toTokenAddress = toTokenAddress,
provider = provider,
selectedFee = selectedFee,
amount = amount,
isAllowedToSpend = isAllowedToSpend,
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
)
}
@ -272,8 +272,8 @@ internal class SwapInteractorImpl @Inject constructor(
toToken = toToken,
provider = provider,
amount = amount,
isAllowedToSpend = isAllowedToSpend,
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
selectedFee = selectedFee,
)
}
}
@ -284,20 +284,21 @@ internal class SwapInteractorImpl @Inject constructor(
networkId: String,
fromToken: CryptoCurrencyStatus,
toToken: CryptoCurrencyStatus,
fromTokenAddress: String,
toTokenAddress: String,
provider: SwapProvider,
selectedFee: FeeType,
amount: SwapAmount,
isAllowedToSpend: Boolean,
isBalanceWithoutFeeEnough: Boolean,
): Pair<SwapProvider, SwapState> {
val fromTokenAddress = getTokenAddress(fromToken.currency)
val isAllowedToSpend = isAllowedToSpend(networkId, fromToken.currency, amount)
if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) {
allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress)
transactionManager.updateWalletManager(networkId, derivationPath)
}
return if (isAllowedToSpend && isBalanceWithoutFeeEnough) {
provider to loadSwapData(
provider = provider,
networkId = networkId,
fromTokenAddress = fromTokenAddress,
toTokenAddress = toTokenAddress,
fromToken = fromToken,
toToken = toToken,
amount = amount,
@ -313,6 +314,7 @@ internal class SwapInteractorImpl @Inject constructor(
isAllowedToSpend = isAllowedToSpend,
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
provider = provider,
selectedFee = selectedFee,
)
}
}
@ -323,8 +325,8 @@ internal class SwapInteractorImpl @Inject constructor(
toToken: CryptoCurrencyStatus,
provider: SwapProvider,
amount: SwapAmount,
isAllowedToSpend: Boolean,
isBalanceWithoutFeeEnough: Boolean,
selectedFee: FeeType,
): Pair<SwapProvider, SwapState> {
return provider to loadQuoteData(
exchangeProviderType = ExchangeProviderType.CEX,
@ -332,9 +334,10 @@ internal class SwapInteractorImpl @Inject constructor(
amount = amount,
fromTokenStatus = fromToken,
toTokenStatus = toToken,
isAllowedToSpend = isAllowedToSpend,
isAllowedToSpend = true,
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
provider = provider,
selectedFee = selectedFee,
)
}
@ -342,7 +345,7 @@ internal class SwapInteractorImpl @Inject constructor(
override suspend fun onSwap(
exchangeProviderType: ExchangeProviderType,
networkId: String,
swapStateData: SwapStateData,
swapData: SwapDataModel,
currencyToSend: CryptoCurrency,
currencyToGet: CryptoCurrency,
amountToSwap: String,
@ -355,7 +358,7 @@ internal class SwapInteractorImpl @Inject constructor(
ExchangeProviderType.DEX -> {
onSwapDex(
networkId = networkId,
swapStateData = swapStateData,
swapData = swapData,
currencyToSend = currencyToSend,
currencyToGet = currencyToGet,
amountToSwap = amountToSwap,
@ -365,9 +368,39 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
override suspend fun updateQuotesStateWithSelectedFee(
state: SwapState.QuotesLoadedState,
selectedFee: FeeType,
fromToken: CryptoCurrencyStatus,
amountToSwap: String,
networkId: String,
): SwapState.QuotesLoadedState {
val amountDecimal = toBigDecimalOrNull(amountToSwap)
if (amountDecimal == null || amountDecimal.signum() == 0) {
return state
}
val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken.currency))
val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = state.txFee)
val isBalanceIncludeFeeEnough =
isBalanceEnough(networkId, fromToken.currency, amount, feeByPriority)
val isFeeEnough = checkFeeIsEnough(
fee = feeByPriority,
spendAmount = amount,
networkId = networkId,
fromToken = fromToken.currency,
)
return state.copy(
permissionState = PermissionDataState.Empty,
preparedSwapConfigState = state.preparedSwapConfigState.copy(
isBalanceEnough = isBalanceIncludeFeeEnough,
isFeeEnough = isFeeEnough,
),
)
}
private suspend fun onSwapDex(
networkId: String,
swapStateData: SwapStateData,
swapData: SwapDataModel,
currencyToSend: CryptoCurrency,
currencyToGet: CryptoCurrency,
amountToSwap: String,
@ -382,8 +415,8 @@ internal class SwapInteractorImpl @Inject constructor(
currencyToSend = swapCurrencyConverter.convert(currencyToSend),
feeAmount = fee.feeValue,
gasLimit = fee.gasLimit,
destinationAddress = swapStateData.swapModel.transaction.txTo,
dataToSign = (swapStateData.swapModel.transaction as ExpressTransactionModel.DEX).txData,
destinationAddress = swapData.transaction.txTo,
dataToSign = (swapData.transaction as ExpressTransactionModel.DEX).txData,
),
isSwap = true,
derivationPath = derivationPath,
@ -405,7 +438,7 @@ internal class SwapInteractorImpl @Inject constructor(
currencyToSend.symbol,
),
toAmount = amountFormatter.formatSwapAmountToUI(
swapStateData.swapModel.toTokenAmount,
swapData.toTokenAmount,
currencyToGet.symbol,
),
txAddress = userWalletManager.getLastTransactionHash(networkId, derivationPath) ?: "",
@ -437,12 +470,6 @@ internal class SwapInteractorImpl @Inject constructor(
return ONE_INCH_SUPPORTED_NETWORKS.contains(networkId)
}
@Deprecated("used in old swap mechanism")
override fun getSwapAmountForToken(amount: String, token: CryptoCurrency): SwapAmount {
val amountDecimal = requireNotNull(toBigDecimalOrNull(amount)) { "wrong amount format" }
return SwapAmount(amountDecimal, getTokenDecimals(token))
}
@Deprecated("used in old swap mechanism")
private suspend fun onSuccessLegacyFlow(currency: CryptoCurrency) {
userWalletManager.addToken(swapCurrencyConverter.convert(currency), derivationPath)
@ -526,6 +553,7 @@ internal class SwapInteractorImpl @Inject constructor(
provider: SwapProvider,
isAllowedToSpend: Boolean,
isBalanceWithoutFeeEnough: Boolean,
selectedFee: FeeType,
): SwapState {
val fromToken = fromTokenStatus.currency
val toToken = toTokenStatus.currency
@ -541,7 +569,7 @@ internal class SwapInteractorImpl @Inject constructor(
rateType = RateType.FLOAT,
)
getState(
getQuotesState(
exchangeProviderType = exchangeProviderType,
quoteDataModel = quotes,
amount = amount,
@ -550,11 +578,13 @@ internal class SwapInteractorImpl @Inject constructor(
networkId = networkId,
isAllowedToSpend = isAllowedToSpend,
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
providerType = provider.type,
selectedFee = selectedFee,
)
}
}
private suspend fun getState(
private suspend fun getQuotesState(
exchangeProviderType: ExchangeProviderType,
quoteDataModel: AggregatedSwapDataModel<QuoteModel>,
amount: SwapAmount,
@ -563,38 +593,59 @@ internal class SwapInteractorImpl @Inject constructor(
networkId: String,
isAllowedToSpend: Boolean,
isBalanceWithoutFeeEnough: Boolean,
providerType: ExchangeProviderType,
selectedFee: FeeType,
): SwapState {
val quoteModel = quoteDataModel.dataModel
if (quoteModel != null) {
val txFee = if (providerType == ExchangeProviderType.CEX) {
getFeeForCex(amount, fromToken, networkId)
} else {
TxFeeState.Empty
}
val swapState = updateBalances(
networkId = networkId,
fromTokenStatus = fromToken,
toTokenStatus = toToken,
fromTokenAmount = amount,
toTokenAmount = quoteModel.toTokenAmount,
swapStateData = null,
swapData = null,
txFeeState = txFee,
)
val quotesState = when (exchangeProviderType) {
return when (exchangeProviderType) {
ExchangeProviderType.DEX -> {
updatePermissionState(
val state = updatePermissionState(
networkId = networkId,
fromToken = fromToken.currency,
swapAmount = amount,
quotesLoadedState = swapState
quotesLoadedState = swapState,
)
state.copy(
preparedSwapConfigState = state.preparedSwapConfigState.copy(
isAllowedToSpend = isAllowedToSpend,
isBalanceEnough = isBalanceWithoutFeeEnough,
),
)
}
ExchangeProviderType.CEX -> {
swapState.copy(permissionState = PermissionDataState.Empty)
val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = txFee)
val isFeeEnough = checkFeeIsEnough(
fee = feeByPriority,
spendAmount = amount,
networkId = networkId,
fromToken = fromToken.currency,
)
swapState.copy(
permissionState = PermissionDataState.Empty,
preparedSwapConfigState = PreparedSwapConfigState(
isFeeEnough = isFeeEnough,
isAllowedToSpend = isAllowedToSpend,
isBalanceEnough = isBalanceWithoutFeeEnough,
),
)
}
}
return quotesState.copy(
preparedSwapConfigState = quotesState.preparedSwapConfigState.copy(
isAllowedToSpend = isAllowedToSpend,
isBalanceEnough = isBalanceWithoutFeeEnough,
),
)
} else {
return SwapState.SwapError(quoteDataModel.error)
}
@ -606,7 +657,7 @@ internal class SwapInteractorImpl @Inject constructor(
val rates = getQuotes(nativeToken.id)
return rates[nativeToken.id]?.fiatRate?.let { rate ->
fees.map { fee ->
" (${fee.toFiatString(rate, appCurrency.symbol, true)})"
fee.toFiatString(rate, appCurrency.symbol, true)
}
}.orEmpty()
}
@ -618,8 +669,6 @@ internal class SwapInteractorImpl @Inject constructor(
private suspend fun loadSwapData(
provider: SwapProvider,
networkId: String,
fromTokenAddress: String,
toTokenAddress: String,
fromToken: CryptoCurrencyStatus,
toToken: CryptoCurrencyStatus,
amount: SwapAmount,
@ -647,11 +696,11 @@ internal class SwapInteractorImpl @Inject constructor(
data = (swapData.transaction as ExpressTransactionModel.DEX).txData,
derivationPath = derivationPath,
)
val txFeeState = proxyFeesToFeeState(networkId, feeData)
val feeByPriority = when (selectedFee) {
FeeType.NORMAL -> txFeeState.normalFee.feeValue
FeeType.PRIORITY -> txFeeState.priorityFee.feeValue
val txFeeState = when (feeData) {
is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(networkId)
is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(networkId)
}
val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = txFeeState)
val isBalanceIncludeFeeEnough =
isBalanceEnough(networkId, fromToken.currency, amount, feeByPriority)
val isFeeEnough = checkFeeIsEnough(
@ -666,10 +715,8 @@ internal class SwapInteractorImpl @Inject constructor(
toTokenStatus = toToken,
fromTokenAmount = amount,
toTokenAmount = swapData.toTokenAmount,
swapStateData = SwapStateData(
fee = txFeeState,
swapModel = swapData,
),
swapData = swapData,
txFeeState = txFeeState,
)
return swapState.copy(
permissionState = PermissionDataState.Empty,
@ -692,7 +739,8 @@ internal class SwapInteractorImpl @Inject constructor(
toTokenStatus: CryptoCurrencyStatus,
fromTokenAmount: SwapAmount,
toTokenAmount: SwapAmount,
swapStateData: SwapStateData?,
swapData: SwapDataModel?,
txFeeState: TxFeeState,
): SwapState.QuotesLoadedState {
val fromToken = fromTokenStatus.currency
val toToken = toTokenStatus.currency
@ -719,11 +767,31 @@ internal class SwapInteractorImpl @Inject constructor(
toRate = rates[toToken.id]?.fiatRate?.toDouble() ?: 0.0,
),
networkCurrency = userWalletManager.getNetworkCurrency(networkId),
swapDataModel = swapStateData,
swapDataModel = swapData,
tangemFee = getTangemFee(),
txFee = txFeeState,
)
}
private suspend fun getFeeForCex(
amount: SwapAmount,
fromToken: CryptoCurrencyStatus,
networkId: String,
): TxFeeState {
getSelectedWalletSyncUseCase().getOrNull()?.walletId?.let { userWalletId ->
val txFeeResult = getFeeUseCase(
amount = amount.value,
destination = fromToken.value.networkAddress?.defaultAddress ?: "",
userWalletId = userWalletId,
cryptoCurrency = fromToken.currency,
).firstOrNull()
txFeeResult?.getOrNull()?.let { txFee ->
return txFee.toTxFeeState(networkId)
}
}
return TxFeeState.Empty
}
@Suppress("LongParameterList")
private suspend fun updatePermissionState(
networkId: String,
@ -759,9 +827,17 @@ internal class SwapInteractorImpl @Inject constructor(
data = transactionData,
derivationPath = derivationPath,
)
val feeState = proxyFeesToFeeState(networkId, feeData)
val feeState = when (feeData) {
is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(networkId)
is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(networkId)
}
val fee = when (feeState) {
TxFeeState.Empty -> BigDecimal.ZERO
is TxFeeState.MultipleFeeState -> feeState.normalFee.feeValue
is TxFeeState.SingleFeeState -> feeState.fee.feeValue
}
val isFeeEnough = checkFeeIsEnough(
fee = feeData.normalFee.fee.value,
fee = fee,
spendAmount = SwapAmount.zeroSwapAmount(),
networkId = networkId,
fromToken = fromToken,
@ -801,30 +877,30 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
private suspend fun proxyFeesToFeeState(networkId: String, proxyFees: ProxyFees): TxFeeState {
val normalFeeValue = proxyFees.minFee.fee.value // in swap for normal use min fee
val normalFeeGas = proxyFees.minFee.gasLimit.toInt()
val priorityFeeValue = proxyFees.normalFee.fee.value // in swap for priority use normal fee
val priorityFeeGas = proxyFees.normalFee.gasLimit.toInt()
private suspend fun ProxyFees.MultipleFees.proxyFeesToFeeState(networkId: String): TxFeeState {
val normalFeeValue = this.minFee.fee.value // in swap for normal use min fee
val normalFeeGas = this.minFee.gasLimit.toInt()
val priorityFeeValue = this.normalFee.fee.value // in swap for priority use normal fee
val priorityFeeGas = this.normalFee.gasLimit.toInt()
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 networkCurrency = userWalletManager.getNetworkCurrency(networkId)
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(
return TxFeeState.MultipleFeeState(
normalFee = TxFee(
feeValue = normalFeeValue,
gasLimit = normalFeeGas,
feeFiatFormatted = normalFiatFee,
feeCryptoFormatted = normalCryptoFee,
cryptoSymbol = networkCurrency,
feeType = FeeType.NORMAL,
),
priorityFee = TxFee(
@ -832,11 +908,108 @@ internal class SwapInteractorImpl @Inject constructor(
gasLimit = priorityFeeGas,
feeFiatFormatted = priorityFiatFee,
feeCryptoFormatted = priorityCryptoFee,
cryptoSymbol = networkCurrency,
feeType = FeeType.PRIORITY,
),
)
}
private suspend fun ProxyFees.SingleFee.proxyFeesToFeeState(networkId: String): TxFeeState {
val normalFeeValue = this.singleFee.fee.value
val normalFeeGas = this.singleFee.gasLimit.toInt()
val networkCurrency = userWalletManager.getNetworkCurrency(networkId)
val feesFiat = getFormattedFiatFees(networkId, normalFeeValue)
val normalFiatFee = requireNotNull(feesFiat.getOrNull(0)) { "feesFiat item 0 couldn't be null" }
val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI(
amount = normalFeeValue,
decimals = transactionManager.getNativeTokenDecimals(networkId),
)
return TxFeeState.SingleFeeState(
fee = TxFee(
feeValue = normalFeeValue,
gasLimit = normalFeeGas,
feeFiatFormatted = normalFiatFee,
feeCryptoFormatted = normalCryptoFee,
cryptoSymbol = networkCurrency,
feeType = FeeType.NORMAL,
),
)
}
private suspend fun TransactionFee.toTxFeeState(networkId: String): TxFeeState {
val networkCurrency = userWalletManager.getNetworkCurrency(networkId)
return when (this) {
is TransactionFee.Choosable -> {
val feeNormal = this.normal.amount.value ?: BigDecimal.ZERO
val feePriority = this.priority.amount.value ?: BigDecimal.ZERO
val normalFiatValue = getFormattedFiatFees(networkId, feeNormal)[0]
val priorityFiatValue = getFormattedFiatFees(networkId, feePriority)[0]
val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI(
amount = feeNormal,
decimals = transactionManager.getNativeTokenDecimals(networkId),
)
val priorityCryptoFee = amountFormatter.formatBigDecimalAmountToUI(
amount = feePriority,
decimals = transactionManager.getNativeTokenDecimals(networkId),
)
TxFeeState.MultipleFeeState(
normalFee = TxFee(
feeValue = feeNormal,
gasLimit = this.normal.getGasLimit(),
feeFiatFormatted = normalFiatValue,
feeCryptoFormatted = normalCryptoFee,
cryptoSymbol = networkCurrency,
feeType = FeeType.NORMAL,
),
priorityFee = TxFee(
feeValue = feePriority,
gasLimit = this.priority.getGasLimit(),
feeFiatFormatted = priorityFiatValue,
feeCryptoFormatted = priorityCryptoFee,
cryptoSymbol = networkCurrency,
feeType = FeeType.PRIORITY,
),
)
}
is TransactionFee.Single -> {
val feeNormal = this.normal.amount.value ?: BigDecimal.ZERO
val normalFiatValue = getFormattedFiatFees(networkId, feeNormal)[0]
val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI(
amount = feeNormal,
decimals = transactionManager.getNativeTokenDecimals(networkId),
)
TxFeeState.SingleFeeState(
fee = TxFee(
feeValue = this.normal.amount.value ?: BigDecimal.ZERO,
gasLimit = this.normal.getGasLimit(),
feeFiatFormatted = normalFiatValue,
feeCryptoFormatted = normalCryptoFee,
cryptoSymbol = networkCurrency,
feeType = FeeType.NORMAL,
),
)
}
}
}
private fun Fee.getGasLimit(): Int {
return when (this) {
is Fee.Common -> 0
is Fee.Ethereum -> this.gasLimit.toInt()
}
}
private fun selectFeeByType(feeType: FeeType, txFeeState: TxFeeState): BigDecimal {
return when (txFeeState) {
TxFeeState.Empty -> BigDecimal.ZERO
is TxFeeState.SingleFeeState -> txFeeState.fee.feeValue
is TxFeeState.MultipleFeeState -> when (feeType) {
FeeType.NORMAL -> txFeeState.normalFee.feeValue
FeeType.PRIORITY -> txFeeState.priorityFee.feeValue
}
}
}
private fun isBalanceEnough(
networkId: String,
fromToken: CryptoCurrency,
@ -866,7 +1039,7 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
override suspend fun checkFeeIsEnough(
private suspend fun checkFeeIsEnough(
fee: BigDecimal?,
spendAmount: SwapAmount,
networkId: String,
@ -938,10 +1111,7 @@ internal class SwapInteractorImpl @Inject constructor(
}
companion object {
private const val DEFAULT_SLIPPAGE = 2
@Suppress("UnusedPrivateMember")
private const val INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT = 1.0 // if need to increase fee when check isEnough
private const val INCREASE_GAS_LIMIT_BY = 112 // 12%
private const val INFINITY_SYMBOL = ""

View file

@ -5,6 +5,7 @@ import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.feature.swap.domain.*
@ -36,6 +37,8 @@ class SwapDomainModule {
@SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
@SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase,
quotesRepository: QuotesRepository,
walletManagersFacade: WalletManagersFacade,
coroutineDispatcherProvider: CoroutineDispatcherProvider,
): SwapInteractor {
return SwapInteractorImpl(
transactionManager = transactionManager,
@ -49,6 +52,8 @@ class SwapDomainModule {
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
getMultiCryptoCurrencyStatusUseCase = getCryptoCurrencyStatusUseCase,
quotesRepository = quotesRepository,
walletManagersFacade = walletManagersFacade,
dispatcher = coroutineDispatcherProvider,
)
}

View file

@ -16,7 +16,7 @@ sealed class ExpressTransactionModel {
override val txTo: String,
val txFrom: String,
val txData: String,
): ExpressTransactionModel()
) : ExpressTransactionModel()
data class CEX(
override val fromAmount: SwapAmount,
@ -25,6 +25,5 @@ sealed class ExpressTransactionModel {
override val txTo: String,
val externalTxId: String,
val externalTxUrl: String,
): ExpressTransactionModel()
) : ExpressTransactionModel()
}

View file

@ -25,7 +25,7 @@ class AmountFormatter {
* @param currency
* @return formatted [String]
*/
fun formatBigDecimalAmountToUI(amount: BigDecimal, decimals: Int, currency: String): String {
fun formatBigDecimalAmountToUI(amount: BigDecimal, decimals: Int, currency: String? = null): String {
return amount.toFormattedCurrencyString(decimals, currency)
}
}

View file

@ -20,7 +20,8 @@ sealed interface SwapState {
isFeeEnough = false,
),
val permissionState: PermissionDataState = PermissionDataState.Empty,
val swapDataModel: SwapStateData? = null,
val swapDataModel: SwapDataModel? = null,
val txFee: TxFeeState,
val tangemFee: Double,
) : SwapState
@ -62,21 +63,30 @@ data class RequestApproveStateData(
val fromTokenAmount: SwapAmount,
)
data class SwapStateData(
val fee: TxFeeState,
val swapModel: SwapDataModel,
)
// data class SwapStateData(
// val fee: TxFeeState,
// val swapModel: SwapDataModel,
// )
data class TxFeeState(
val normalFee: TxFee,
val priorityFee: TxFee,
)
sealed class TxFeeState {
data class MultipleFeeState(
val normalFee: TxFee,
val priorityFee: TxFee,
) : TxFeeState()
data class SingleFeeState(
val fee: TxFee,
) : TxFeeState()
object Empty : TxFeeState()
}
data class TxFee(
val feeValue: BigDecimal,
val gasLimit: Int,
val feeFiatFormatted: String,
val feeCryptoFormatted: String,
val cryptoSymbol: String,
val feeType: FeeType,
)

View file

@ -6,6 +6,7 @@ import com.tangem.core.ui.components.notifications.NotificationConfig
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
import com.tangem.feature.swap.models.states.FeeItemState
import com.tangem.feature.swap.models.states.ProviderState
data class SwapStateHolder(
@ -14,13 +15,14 @@ data class SwapStateHolder(
val networkCurrency: String,
val networkId: String,
val blockchainId: String, // not the same as networkId, its local id in app
val fee: FeeState = FeeState.Empty,
val warnings: List<SwapWarning> = emptyList(),
val alert: SwapWarning.GenericWarning? = null,
val updateInProgress: Boolean = false,
val providerState: ProviderState,
val fee: FeeItemState = FeeItemState.Empty,
val permissionState: SwapPermissionState = SwapPermissionState.Empty,
val successState: SwapSuccessStateHolder? = null,
val selectTokenState: SwapSelectTokenStateHolder? = null,
val bottomSheetConfig: TangemBottomSheetConfig? = null,

View file

@ -1,7 +1,5 @@
package com.tangem.feature.swap.models
import com.tangem.core.ui.components.states.Item
import com.tangem.feature.swap.domain.models.ui.FeeType
import com.tangem.feature.swap.domain.models.ui.TxFee
data class UiActions(
@ -17,10 +15,9 @@ data class UiActions(
val openPermissionBottomSheet: () -> Unit,
val hidePermissionBottomSheet: () -> Unit,
val onChangeApproveType: (ApproveType) -> Unit,
val onSelectItemFee: (Item<TxFee>) -> Unit,
// region new actions
val onClickFee: () -> Unit,
val onSelectFeeType: (FeeType) -> Unit,
val onSelectFeeType: (TxFee) -> Unit,
val onProviderClick: (String) -> Unit,
val onProviderSelect: (String) -> Unit,
)

View file

@ -4,8 +4,8 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.feature.swap.domain.models.ui.FeeType
import kotlinx.collections.immutable.ImmutableList
class ChooseFeeBottomSheetConfig(
data class ChooseFeeBottomSheetConfig(
val selectedFee: FeeType,
val onSelectFeeType: (FeeType) -> Unit,
val feeItems: ImmutableList<FeeItemState>,
val feeItems: ImmutableList<FeeItemState.Content>,
) : TangemBottomSheetConfigContent

View file

@ -1,13 +1,19 @@
package com.tangem.feature.swap.models.states
import com.tangem.core.ui.extensions.TextReference
import com.tangem.feature.swap.domain.models.ui.FeeType
data class FeeItemState(
val feeType: FeeType,
val title: String,
val amountCrypto: String,
val symbolCrypto: String,
val amountFiat: String,
val symbolFiat: String,
val onClick: () -> Unit,
)
sealed class FeeItemState {
data class Content(
val feeType: FeeType,
val title: TextReference,
val amountCrypto: String,
val symbolCrypto: String,
val amountFiatFormatted: String,
val isClickable: Boolean,
val onClick: () -> Unit,
) : FeeItemState()
object Empty : FeeItemState()
}

View file

@ -14,6 +14,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.rows.SelectorRowItem
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.domain.models.ui.FeeType
import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig
@ -31,7 +32,7 @@ fun ChooseFeeBottomSheet(config: TangemBottomSheetConfig) {
@Composable
private fun ChooseFeeBottomSheetContent(content: ChooseFeeBottomSheetConfig) {
Column(
modifier = Modifier.background(TangemTheme.colors.background.secondary),
modifier = Modifier.background(TangemTheme.colors.background.primary),
) {
Text(
text = "Choose fee", // todo replace with strings
@ -73,7 +74,7 @@ private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) {
content.feeItems.forEach { feeItem ->
val isSelected = feeItem.feeType == content.selectedFee
val preEllipsizeText = feeItem.amountCrypto
val postEllipsizeText = "${feeItem.symbolCrypto} (${feeItem.amountFiat}${feeItem.symbolFiat})"
val postEllipsizeText = "${feeItem.symbolCrypto} (${feeItem.amountFiatFormatted})"
when (feeItem.feeType) {
FeeType.NORMAL -> {
SelectorRowItem(
@ -103,22 +104,22 @@ private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) {
@Composable
private fun ChooseFeeBottomSheetContent_Preview() {
val feeItems = listOf(
FeeItemState(
FeeItemState.Content(
feeType = FeeType.NORMAL,
title = "Fee",
title = stringReference("Fee"),
amountCrypto = "1000",
symbolCrypto = "MATIC",
amountFiat = "10",
symbolFiat = "$",
amountFiatFormatted = "(10$)",
isClickable = false,
onClick = {},
),
FeeItemState(
FeeItemState.Content(
feeType = FeeType.PRIORITY,
title = "Fee",
title = stringReference("Fee"),
amountCrypto = "2000",
symbolCrypto = "MATIC",
amountFiat = "20",
symbolFiat = "$",
amountFiatFormatted = "(10$)",
isClickable = false,
onClick = {},
),
).toImmutableList()

View file

@ -5,35 +5,47 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.rows.SimpleActionRow
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.domain.models.ui.FeeType
import com.tangem.feature.swap.models.states.FeeItemState
@Composable
fun FeeItem(state: FeeItemState) {
fun FeeItemBlock(state: FeeItemState) {
if (state is FeeItemState.Content) {
FeeItem(state = state)
}
}
@Composable
fun FeeItem(state: FeeItemState.Content) {
Box(
modifier = Modifier
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.clip(shape = TangemTheme.shapes.roundedCornersXMedium)
.clickable(
onClick = state.onClick,
)
.fillMaxWidth()
.defaultMinSize(minHeight = TangemTheme.dimens.size68),
) {
val description = "${state.amountCrypto}${state.symbolCrypto} (${state.amountFiat}${state.symbolFiat})"
val description = "${state.amountCrypto}${state.symbolCrypto} (${state.amountFiatFormatted})"
SimpleActionRow(
modifier = Modifier.padding(
start = TangemTheme.dimens.spacing12,
top = TangemTheme.dimens.spacing12,
),
title = state.title,
title = state.title.resolveReference(),
description = description,
isClickable = state.isClickable,
)
}
}
@ -41,13 +53,13 @@ fun FeeItem(state: FeeItemState) {
@Preview
@Composable
private fun FeeItemPreview() {
val state = FeeItemState(
val state = FeeItemState.Content(
feeType = FeeType.NORMAL,
title = "Fee",
title = stringReference("Fee"),
amountCrypto = "1000",
symbolCrypto = "MATIC",
amountFiat = "10",
symbolFiat = "$",
amountFiatFormatted = "(1000$)",
isClickable = false,
onClick = {},
)
Column {

View file

@ -5,8 +5,6 @@ import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.common.Provider
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.notifications.NotificationConfig
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.resourceReference
import com.tangem.core.ui.extensions.stringReference
@ -22,9 +20,7 @@ import com.tangem.feature.swap.domain.models.domain.SwapProvider
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
import com.tangem.feature.swap.domain.models.ui.*
import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig
import com.tangem.feature.swap.models.states.GivePermissionBottomSheetConfig
import com.tangem.feature.swap.models.states.ProviderState
import com.tangem.feature.swap.models.states.*
import com.tangem.feature.swap.presentation.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@ -76,7 +72,7 @@ internal class StateBuilder(
coinId = null,
isBalanceHidden = true,
),
fee = FeeState.Loading,
fee = FeeItemState.Empty,
networkCurrency = networkInfo.blockchainCurrency,
swapButton = SwapButton(enabled = false, loading = true, onClick = {}),
onRefresh = {},
@ -86,7 +82,7 @@ internal class StateBuilder(
updateInProgress = true,
onShowPermissionBottomSheet = actions.openPermissionBottomSheet,
onCancelPermissionBottomSheet = actions.hidePermissionBottomSheet,
providerState = ProviderState.Loading(),
providerState = ProviderState.Empty(),
)
}
@ -127,7 +123,7 @@ internal class StateBuilder(
),
),
),
fee = FeeState.Empty,
fee = FeeItemState.Empty,
swapButton = SwapButton(
enabled = false,
loading = false,
@ -143,8 +139,8 @@ internal class StateBuilder(
toToken: CryptoCurrency,
mainTokenId: String,
): SwapStateHolder {
val canSelectSendToken = mainTokenId != fromToken.id.value // TODO look at id matching
val canSelectReceiveToken = mainTokenId != toToken.id.value // TODO look at id matching
val canSelectSendToken = mainTokenId != fromToken.id.value
val canSelectReceiveToken = mainTokenId != toToken.id.value
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
return uiStateHolder.copy(
@ -172,8 +168,9 @@ internal class StateBuilder(
balance = if (!canSelectReceiveToken) uiStateHolder.receiveCardData.balance else "",
isBalanceHidden = isBalanceHiddenProvider(),
),
fee = FeeState.Loading,
fee = FeeItemState.Empty,
swapButton = SwapButton(enabled = false, loading = true, onClick = {}),
providerState = ProviderState.Loading(),
permissionState = uiStateHolder.permissionState,
updateInProgress = true,
)
@ -185,7 +182,6 @@ internal class StateBuilder(
* @param uiStateHolder whole screen state
* @param quoteModel data model
* @param fromToken token data to swap
* @param onFeeSetup callback for reset fee after auto update
* @return updated whole screen state
*/
@Suppress("LongMethod")
@ -194,7 +190,7 @@ internal class StateBuilder(
quoteModel: SwapState.QuotesLoadedState,
fromToken: CryptoCurrency,
swapProvider: SwapProvider,
onFeeSetup: (TxFee) -> Unit,
selectedFeeType: FeeType,
): SwapStateHolder {
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
@ -221,7 +217,7 @@ internal class StateBuilder(
),
)
}
val feeState = createFeeState(quoteModel, uiStateHolder, onFeeSetup)
val feeState = createFeeState(quoteModel.txFee, selectedFeeType)
val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus
val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus
return uiStateHolder.copy(
@ -254,7 +250,6 @@ internal class StateBuilder(
permissionState = convertPermissionState(
lastPermissionState = uiStateHolder.permissionState,
permissionDataState = quoteModel.permissionState,
feeState = feeState,
onGivePermissionClick = actions.onGivePermissionClick,
onChangeApproveType = actions.onChangeApproveType,
),
@ -308,7 +303,7 @@ internal class StateBuilder(
isBalanceHidden = isBalanceHiddenProvider(),
),
warnings = emptyList(),
fee = FeeState.Empty,
fee = FeeItemState.Empty,
swapButton = SwapButton(
enabled = false,
loading = false,
@ -397,144 +392,36 @@ internal class StateBuilder(
}
}
fun updateFeeSelectedItem(uiState: SwapStateHolder, item: Item<TxFee>, isFeeEnough: Boolean): SwapStateHolder {
val newSelectedItem = item.copy(
startText = TextReference.Res(R.string.send_network_fee_title),
)
val permissionState = uiState.permissionState
val newPermissionState = if (permissionState is SwapPermissionState.ReadyForRequest) {
permissionState.copy(
fee = newSelectedItem.endText,
)
} else {
permissionState
}
val updateState = when (val fee = uiState.fee) {
is FeeState.Loaded -> {
getUpdatedFeeStateForEnoughFee(uiState, fee, item, newSelectedItem, newPermissionState, isFeeEnough)
private fun createFeeState(txFeeState: TxFeeState, feeType: FeeType): FeeItemState {
val isClickable: Boolean
val fee = when (txFeeState) {
TxFeeState.Empty -> return FeeItemState.Empty
is TxFeeState.SingleFeeState -> {
isClickable = false
txFeeState.fee
}
is FeeState.NotEnoughFundsWarning -> {
getUpdatedFeeStateForNotEnoughFee(uiState, fee, item, newSelectedItem, newPermissionState, isFeeEnough)
is TxFeeState.MultipleFeeState -> {
isClickable = true
when (feeType) {
FeeType.NORMAL -> {
txFeeState.normalFee
}
FeeType.PRIORITY -> {
txFeeState.priorityFee
}
}
}
else -> uiState
}
return if (isFeeEnough) {
updateState.copy(
warnings = uiState.warnings.filterNot { it is SwapWarning.InsufficientFunds },
)
} else {
updateState.copy(
warnings = uiState.warnings.plus(SwapWarning.InsufficientFunds),
)
}
}
@Suppress("LongParameterList")
private fun getUpdatedFeeStateForEnoughFee(
uiState: SwapStateHolder,
fee: FeeState.Loaded,
itemToSelect: Item<TxFee>,
newSelectedItem: Item<TxFee>,
newPermissionState: SwapPermissionState,
isFeeEnough: Boolean,
): SwapStateHolder {
val newState = fee.state?.copy(
selectedItem = newSelectedItem,
items = selectNewItem(fee.state.items, itemToSelect),
return FeeItemState.Content(
feeType = feeType,
title = stringReference("Fee"), // todo replace with string
amountCrypto = fee.feeCryptoFormatted,
symbolCrypto = fee.cryptoSymbol,
amountFiatFormatted = fee.feeFiatFormatted,
isClickable = isClickable,
onClick = actions.onClickFee,
)
val newFeeState = if (isFeeEnough) {
fee.copy(state = newState)
} else {
FeeState.NotEnoughFundsWarning(
tangemFee = fee.tangemFee,
state = newState,
onSelectItem = fee.onSelectItem,
)
}
return uiState.copy(
fee = newFeeState,
permissionState = newPermissionState,
swapButton = uiState.swapButton.copy(
enabled = isFeeEnough,
),
)
}
@Suppress("LongParameterList")
private fun getUpdatedFeeStateForNotEnoughFee(
uiState: SwapStateHolder,
fee: FeeState.NotEnoughFundsWarning,
itemToSelect: Item<TxFee>,
newSelectedItem: Item<TxFee>,
newPermissionState: SwapPermissionState,
isFeeEnough: Boolean,
): SwapStateHolder {
val newState = fee.state?.copy(
selectedItem = newSelectedItem,
items = selectNewItem(fee.state.items, itemToSelect),
)
val newFeeState = if (isFeeEnough) {
FeeState.Loaded(
tangemFee = fee.tangemFee,
state = newState,
onSelectItem = fee.onSelectItem,
)
} else {
fee.copy(state = newState)
}
return uiState.copy(
fee = newFeeState,
permissionState = newPermissionState,
swapButton = uiState.swapButton.copy(
enabled = isFeeEnough,
),
)
}
private fun createFeeState(
quoteModel: SwapState.QuotesLoadedState,
uiStateHolder: SwapStateHolder,
onFeeSetup: (TxFee) -> Unit,
): FeeState {
val previousFeeState = when (val stateFee = uiStateHolder.fee) {
is FeeState.Loaded -> stateFee.state
is FeeState.NotEnoughFundsWarning -> stateFee.state
else -> null
}
val permissionState = quoteModel.permissionState
val feeState = if (permissionState is PermissionDataState.PermissionReadyForRequest) {
permissionState.requestApproveData.fee
} else {
quoteModel.swapDataModel?.fee
}
val selectFeeState = createSelectFeeState(
fee = feeState,
previousState = previousFeeState,
onFeeSetup = onFeeSetup,
)
return if (quoteModel.preparedSwapConfigState.isFeeEnough) {
FeeState.Loaded(
tangemFee = quoteModel.tangemFee,
state = selectFeeState,
onSelectItem = actions.onSelectItemFee,
)
} else {
FeeState.NotEnoughFundsWarning(
tangemFee = quoteModel.tangemFee,
state = selectFeeState,
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 {
@ -612,7 +499,6 @@ internal class StateBuilder(
private fun convertPermissionState(
lastPermissionState: SwapPermissionState,
permissionDataState: PermissionDataState,
feeState: FeeState,
onGivePermissionClick: () -> Unit,
onChangeApproveType: (ApproveType) -> Unit,
): SwapPermissionState {
@ -621,97 +507,33 @@ internal class StateBuilder(
} else {
ApproveType.UNLIMITED
}
val fee = when (feeState) {
is FeeSelectState -> feeState.state?.selectedItem?.endText
else -> null
}
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 = fee ?: TextReference.Str(""),
approveButton = ApprovePermissionButton(
enabled = true,
onClick = onGivePermissionClick,
),
cancelButton = CancelPermissionButton(
enabled = true,
),
onChangeApproveType = onChangeApproveType,
)
}
}
private fun createSelectFeeState(
fee: TxFeeState?,
previousState: SelectableItemsState<TxFee>?,
onFeeSetup: (TxFee) -> Unit,
): SelectableItemsState<TxFee>? {
if (fee == null) return null
if (previousState == null) {
onFeeSetup.invoke(fee.normalFee) // if there is no previous state, setup normal fee by default
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) {
onFeeSetup.invoke(fee.normalFee)
normalFeeItem.endText
} else {
onFeeSetup.invoke(fee.priorityFee)
priorityFeeItem.endText
is PermissionDataState.PermissionReadyForRequest -> {
val permissionFee = when (val fee = permissionDataState.requestApproveData.fee) {
TxFeeState.Empty -> error("Fee shouldn't be empty")
is TxFeeState.MultipleFeeState -> fee.priorityFee
is TxFeeState.SingleFeeState -> fee.fee
}
SwapPermissionState.ReadyForRequest(
currency = permissionDataState.currency,
amount = permissionDataState.amount,
approveType = approveType,
walletAddress = getShortAddressValue(permissionDataState.walletAddress),
spenderAddress = getShortAddressValue(permissionDataState.spenderAddress),
fee = TextReference.Str("${permissionFee.feeCryptoFormatted} (${permissionFee.feeFiatFormatted})"),
approveButton = ApprovePermissionButton(
enabled = true,
onClick = onGivePermissionClick,
),
cancelButton = CancelPermissionButton(
enabled = true,
),
onChangeApproveType = onChangeApproveType,
)
}
return previousState.copy(
selectedItem = previousState.selectedItem.copy(
endText = selectedEndText,
),
items = listOf(normalFeeItem, priorityFeeItem).toImmutableList(),
)
}
}
@ -775,6 +597,70 @@ internal class StateBuilder(
}
}
fun showSelectFeeBottomSheet(
uiState: SwapStateHolder,
selectedFee: FeeType,
txFeeState: TxFeeState.MultipleFeeState,
onDismiss: () -> Unit,
): SwapStateHolder {
val config = ChooseFeeBottomSheetConfig(
selectedFee = selectedFee,
onSelectFeeType = {
val selectedItem = when (it) {
FeeType.NORMAL -> txFeeState.normalFee
FeeType.PRIORITY -> txFeeState.priorityFee
}
actions.onSelectFeeType.invoke(selectedItem)
},
feeItems = txFeeState.toFeeItemState(),
)
return uiState.copy(
bottomSheetConfig = TangemBottomSheetConfig(
isShow = true,
onDismissRequest = onDismiss,
content = config,
),
)
}
fun updateSelectedFee(uiState: SwapStateHolder, selectedFee: FeeType): SwapStateHolder {
val config = uiState.bottomSheetConfig?.content as? ChooseFeeBottomSheetConfig
return if (config != null) {
uiState.copy(
bottomSheetConfig = uiState.bottomSheetConfig.copy(
content = config.copy(
selectedFee = selectedFee,
),
),
)
} else {
uiState
}
}
private fun TxFeeState.MultipleFeeState.toFeeItemState(): ImmutableList<FeeItemState.Content> {
return listOf(
FeeItemState.Content(
feeType = this.normalFee.feeType,
title = stringReference("Fee"), // todo replace with string
amountCrypto = this.normalFee.feeCryptoFormatted,
symbolCrypto = this.normalFee.cryptoSymbol,
amountFiatFormatted = this.normalFee.feeFiatFormatted,
isClickable = true,
onClick = {},
),
FeeItemState.Content(
feeType = this.priorityFee.feeType,
title = stringReference("Fee"), // todo replace with string
amountCrypto = this.priorityFee.feeCryptoFormatted,
symbolCrypto = this.priorityFee.cryptoSymbol,
amountFiatFormatted = this.priorityFee.feeFiatFormatted,
isClickable = true,
onClick = {},
),
).toImmutableList()
}
private fun Map.Entry<SwapProvider, SwapState>.convertToProviderState(
onProviderSelect: (String) -> Unit,
): ProviderState? {

View file

@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.models.SwapStateHolder
import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig
import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig
import com.tangem.feature.swap.models.states.GivePermissionBottomSheetConfig
@ -32,6 +33,9 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) {
is ChooseProviderBottomSheetConfig -> {
ChooseProviderBottomSheet(config = config)
}
is ChooseFeeBottomSheetConfig -> {
ChooseFeeBottomSheet(config = config)
}
}
}
}

View file

@ -21,20 +21,15 @@ import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
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.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.domain.models.ui.FeeType
import com.tangem.feature.swap.domain.models.ui.TxFee
import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.models.states.FeeItemState
import com.tangem.feature.swap.models.states.ProviderState
import com.tangem.feature.swap.presentation.R
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal
@Suppress("LongMethod")
@Composable
@ -77,7 +72,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi
),
)
FeeItem(feeState = state.fee, currency = state.networkCurrency)
FeeItemBlock(state = state.fee)
if (state.warnings.isNotEmpty()) SwapWarnings(warnings = state.warnings)
@ -249,49 +244,6 @@ private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) {
}
}
@Composable
private fun FeeItem(feeState: FeeState, currency: String) {
val titleString = stringResource(id = R.string.send_network_fee_title)
val disclaimer = stringResource(id = R.string.swapping_tangem_fee_disclaimer, "${feeState.tangemFee}%")
when (feeState) {
is FeeState.Loaded -> {
if (feeState.state != null) {
SelectableInfoCard(
state = feeState.state,
disclaimer = disclaimer,
onSelect = feeState.onSelectItem,
)
}
}
FeeState.Loading -> {
SmallInfoCardWithDisclaimer(
startText = titleString,
endText = "",
disclaimer = disclaimer,
isLoading = true,
)
}
is FeeState.NotEnoughFundsWarning -> {
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,
)
}
}
is FeeState.Empty -> {
// show nothing
// SmallInfoCard(startText = titleString, endText = "")
}
}
}
@Composable
private fun SwapWarnings(warnings: List<SwapWarning>) {
Column(
@ -400,58 +352,18 @@ private val receiveCard = SwapCardState.SwapCardData(
isBalanceHidden = false,
)
val stateSelectable = SelectableItemsState(
selectedItem = Item(
0,
TextReference.Str("Balance"),
TextReference.Str("0.4405434 BTC"),
true,
TxFee(
feeValue = BigDecimal.ZERO,
gasLimit = 0,
feeFiatFormatted = "",
feeCryptoFormatted = "",
feeType = FeeType.NORMAL,
),
),
items = listOf(
Item(
0,
TextReference.Str("Normal"),
TextReference.Str("0.4405434 BTC"),
true,
TxFee(
feeValue = BigDecimal.ZERO,
gasLimit = 0,
feeFiatFormatted = "",
feeCryptoFormatted = "",
feeType = FeeType.NORMAL,
),
),
Item(
1,
TextReference.Str("Priority"),
TextReference.Str("0.46 BTC"),
false,
TxFee(
feeValue = BigDecimal.ZERO,
gasLimit = 0,
feeFiatFormatted = "",
feeCryptoFormatted = "",
feeType = FeeType.NORMAL,
),
),
).toImmutableList(),
)
private val state = SwapStateHolder(
networkId = "ethereum",
sendCardData = sendCard,
receiveCardData = receiveCard,
fee = FeeState.Loaded(
tangemFee = 0.0,
state = stateSelectable,
onSelectItem = {},
fee = FeeItemState.Content(
feeType = FeeType.NORMAL,
title = stringReference("Fee"),
amountCrypto = "100",
symbolCrypto = "1000",
amountFiatFormatted = "(100)",
isClickable = true,
onClick = {},
),
warnings = listOf(
SwapWarning.PermissionNeeded(

View file

@ -1,25 +1,29 @@
package com.tangem.feature.swap.viewmodels
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import com.tangem.feature.swap.domain.models.domain.SwapProvider
import com.tangem.feature.swap.domain.models.ui.*
import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData
import com.tangem.feature.swap.domain.models.ui.SwapState
import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress
import com.tangem.feature.swap.domain.models.ui.TxFee
data class SwapProcessDataState(
// Initial network id
val networkId: String,
@Deprecated("used in old swap mechanism")
val fromCurrency: Currency? = null,
@Deprecated("used in old swap mechanism")
val toCurrency: Currency? = null,
val fromCryptoCurrency: CryptoCurrencyStatus? = null,
val toCryptoCurrency: CryptoCurrencyStatus? = null,
// Amount from input
val amount: String? = null,
val approveDataModel: RequestApproveStateData? = null,
val swapDataModel: SwapStateData? = null,
val selectedFee: TxFee? = null,
val swapDataModel: SwapDataModel? = null,
val selectedFee: TxFee? = null, // todo
val tokensDataState: TokensDataStateExpress? = null,
val selectedProvider: SwapProvider? = null,
val lastLoadedSwapStates: Map<SwapProvider, SwapState> = emptyMap(),
)
) {
fun getCurrentLoadedSwapState(): SwapState.QuotesLoadedState? {
return lastLoadedSwapStates[selectedProvider] as? SwapState.QuotesLoadedState
}
}

View file

@ -18,6 +18,7 @@ import com.tangem.feature.swap.analytics.SwapEvents
import com.tangem.feature.swap.domain.BlockchainInteractor
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.feature.swap.domain.models.domain.PermissionOptions
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import com.tangem.feature.swap.domain.models.domain.SwapProvider
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
import com.tangem.feature.swap.domain.models.ui.*
@ -271,17 +272,14 @@ internal class SwapViewModel @Inject constructor(
private fun setupLoadedState(provider: SwapProvider, state: SwapState, fromToken: CryptoCurrencyStatus) {
when (state) {
is SwapState.QuotesLoadedState -> {
fillDataState(state.permissionState, state.swapDataModel)
fillLoadedDataState(state, state.permissionState, state.swapDataModel)
uiState = stateBuilder.createQuotesLoadedState(
uiStateHolder = uiState,
quoteModel = state,
fromToken = fromToken.currency,
swapProvider = provider,
) { updatedFee ->
dataState = dataState.copy(
selectedFee = updatedFee,
)
}
selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL,
)
}
is SwapState.EmptyAmountState -> {
uiState = stateBuilder.createQuotesEmptyAmountState(
@ -305,7 +303,11 @@ internal class SwapViewModel @Inject constructor(
return state.entries.first { it.key == selectedSwapProvider }.toPair()
}
private fun fillDataState(permissionState: PermissionDataState, swapDataModel: SwapStateData?) {
private fun fillLoadedDataState(
state: SwapState.QuotesLoadedState,
permissionState: PermissionDataState,
swapDataModel: SwapDataModel?,
) {
dataState = if (permissionState is PermissionDataState.PermissionReadyForRequest) {
dataState.copy(
approveDataModel = permissionState.requestApproveData,
@ -313,11 +315,24 @@ internal class SwapViewModel @Inject constructor(
} else {
dataState.copy(
swapDataModel = swapDataModel,
selectedFee = swapDataModel?.fee?.normalFee,
selectedFee = selectDefaultFee(state),
)
}
}
private fun selectDefaultFee(state: SwapState.QuotesLoadedState): TxFee? {
return dataState.selectedFee
?: when (val txFee = state.txFee) {
TxFeeState.Empty -> null
is TxFeeState.MultipleFeeState -> {
txFee.normalFee
}
is TxFeeState.SingleFeeState -> {
txFee.fee
}
}
}
private fun onSwapClick() {
singleTaskScheduler.cancelTask()
uiState = stateBuilder.createSwapInProgressState(uiState)
@ -326,7 +341,7 @@ internal class SwapViewModel @Inject constructor(
swapInteractor.onSwap(
exchangeProviderType = requireNotNull(dataState.selectedProvider?.type),
networkId = dataState.networkId,
swapStateData = requireNotNull(dataState.swapDataModel),
swapData = requireNotNull(dataState.swapDataModel),
currencyToSend = requireNotNull(dataState.fromCryptoCurrency?.currency),
currencyToGet = requireNotNull(dataState.toCryptoCurrency?.currency),
amountToSwap = requireNotNull(dataState.amount),
@ -539,8 +554,8 @@ internal class SwapViewModel @Inject constructor(
onAmountChanged = { onAmountChanged(it) },
onSwapClick = {
onSwapClick()
val sendTokenSymbol = dataState.fromCurrency?.symbol
val receiveTokenSymbol = dataState.toCurrency?.symbol
val sendTokenSymbol = dataState.fromCryptoCurrency?.currency?.symbol
val receiveTokenSymbol = dataState.toCryptoCurrency?.currency?.symbol
if (sendTokenSymbol != null && receiveTokenSymbol != null) {
analyticsEventHandler.send(
SwapEvents.ButtonSwapClicked(
@ -552,8 +567,8 @@ internal class SwapViewModel @Inject constructor(
},
onGivePermissionClick = {
givePermissionsToSwap()
val sendTokenSymbol = dataState.fromCurrency?.symbol
val receiveTokenSymbol = dataState.toCurrency?.symbol
val sendTokenSymbol = dataState.fromCryptoCurrency?.currency?.symbol
val receiveTokenSymbol = dataState.toCryptoCurrency?.currency?.symbol
if (sendTokenSymbol != null && receiveTokenSymbol != null) {
analyticsEventHandler.send(
SwapEvents.ButtonPermissionApproveClicked(
@ -590,26 +605,36 @@ internal class SwapViewModel @Inject constructor(
onChangeApproveType = { approveType ->
uiState = stateBuilder.updateApproveType(uiState, approveType)
},
onSelectItemFee = { feeItem ->
dataState = dataState.copy(selectedFee = feeItem.data)
val spendAmount = dataState.amount?.let { amount ->
val fromToken = dataState.fromCryptoCurrency ?: return@let null
swapInteractor.getSwapAmountForToken(amount, fromToken.currency)
} ?: dataState.approveDataModel?.fromTokenAmount
spendAmount ?: return@UiActions
val fromToken = dataState.fromCryptoCurrency ?: return@UiActions
viewModelScope.launch(dispatchers.io) {
val isFeeEnough = swapInteractor.checkFeeIsEnough(
fee = feeItem.data.feeValue,
spendAmount = spendAmount,
networkId = dataState.networkId,
fromToken = fromToken.currency,
)
uiState = stateBuilder.updateFeeSelectedItem(uiState, feeItem, isFeeEnough)
onClickFee = {
val selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL
val txFeeState = dataState.getCurrentLoadedSwapState()?.txFee as? TxFeeState.MultipleFeeState
?: return@UiActions
uiState = stateBuilder.showSelectFeeBottomSheet(
uiState = uiState,
selectedFee = selectedFee,
txFeeState = txFeeState,
) {
uiState = stateBuilder.dismissBottomSheet(uiState)
}
},
onSelectFeeType = {
val state = dataState.getCurrentLoadedSwapState() ?: return@UiActions
val fromToken = dataState.fromCryptoCurrency ?: return@UiActions
val amountToSwap = dataState.amount ?: return@UiActions
val selectedProvider = dataState.selectedProvider ?: return@UiActions
uiState = stateBuilder.updateSelectedFee(uiState, it.feeType)
dataState = dataState.copy(selectedFee = it)
viewModelScope.launch(dispatchers.io) {
val updatedState = swapInteractor.updateQuotesStateWithSelectedFee(
state = state,
selectedFee = it.feeType,
fromToken = fromToken,
amountToSwap = amountToSwap,
networkId = dataState.networkId,
)
setupLoadedState(selectedProvider, updatedState, fromToken)
}
},
onClickFee = {},
onSelectFeeType = {},
onProviderClick = {
uiState = stateBuilder.showSelectProviderBottomSheet(
uiState = uiState,

View file

@ -1,7 +1,14 @@
package com.tangem.lib.crypto.models
data class ProxyFees(
val minFee: ProxyFee,
val normalFee: ProxyFee,
val priorityFee: ProxyFee,
)
sealed class ProxyFees {
data class MultipleFees(
val minFee: ProxyFee,
val normalFee: ProxyFee,
val priorityFee: ProxyFee,
) : ProxyFees()
data class SingleFee(
val singleFee: ProxyFee,
) : ProxyFees()
}