diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/EstimateFeeForGaslessTxUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/EstimateFeeForGaslessTxUseCase.kt index 3d375e367d..b43a368305 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/EstimateFeeForGaslessTxUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/EstimateFeeForGaslessTxUseCase.kt @@ -44,12 +44,12 @@ class EstimateFeeForGaslessTxUseCase( suspend operator fun invoke( userWallet: UserWallet, amount: BigDecimal, - cryptoCurrencyStatus: CryptoCurrencyStatus, + sendingTokenCurrencyStatus: CryptoCurrencyStatus, ): Either { return either { catch( block = { - val network = cryptoCurrencyStatus.currency.network + val network = sendingTokenCurrencyStatus.currency.network val nativeCurrency = currenciesRepository.getNetworkCoin( userWalletId = userWallet.walletId, networkId = network.id, @@ -60,7 +60,7 @@ class EstimateFeeForGaslessTxUseCase( estimateFeeUseCase.invoke( userWallet = userWallet, amount = amount, - cryptoCurrencyStatus = cryptoCurrencyStatus, + cryptoCurrencyStatus = sendingTokenCurrencyStatus, ).fold( ifLeft = { raise(it) }, ifRight = { fee -> @@ -77,7 +77,7 @@ class EstimateFeeForGaslessTxUseCase( val initialFee = tokenFeeCalculator.estimateInitialFee( userWallet = userWallet, amount = amount, - tokenCurrencyStatus = cryptoCurrencyStatus, + txTokenCurrencyStatus = sendingTokenCurrencyStatus, ).bind() selectFeePaymentStrategy( diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/EstimateFeeForTokenUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/EstimateFeeForTokenUseCase.kt index a3165f0349..567aa6a486 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/EstimateFeeForTokenUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/EstimateFeeForTokenUseCase.kt @@ -6,10 +6,7 @@ import arrow.core.raise.catch import arrow.core.raise.either import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.common.transaction.Fee -import com.tangem.blockchain.extensions.Result -import com.tangem.domain.demo.DemoTransactionSender import com.tangem.domain.demo.models.DemoConfig -import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet @@ -19,10 +16,8 @@ import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.transaction.GaslessTransactionRepository import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.error.GetFeeError.GaslessError -import com.tangem.domain.transaction.error.mapToFeeError import com.tangem.domain.transaction.models.TransactionFeeExtended import com.tangem.domain.transaction.raiseIllegalStateError -import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.walletmanager.WalletManagersFacade import java.math.BigDecimal @@ -43,38 +38,23 @@ class EstimateFeeForTokenUseCase( suspend operator fun invoke( userWallet: UserWallet, - tokenCurrencyStatus: CryptoCurrencyStatus, + feeTokenCurrencyStatus: CryptoCurrencyStatus, + sendingTokenCurrencyStatus: CryptoCurrencyStatus, amount: BigDecimal, ): Either { return either { catch( block = { - val token = tokenCurrencyStatus.currency + val token = feeTokenCurrencyStatus.currency if (!currencyChecksRepository.isNetworkSupportedForGaslessTx(token.network)) { - raise(GetFeeError.GaslessError.NetworkIsNotSupported) + raise(GaslessError.NetworkIsNotSupported) } - val amountData = amount.convertToSdkAmount(tokenCurrencyStatus) - val result = if (userWallet is UserWallet.Cold && - demoConfig.isDemoCardId(userWallet.scanResponse.card.cardId) - ) { - demoTransactionSender(userWallet, token).estimateFee( - amount = amountData, - destination = "", - ) - } else { - walletManagersFacade.estimateFee( - amount = amountData, - userWalletId = userWallet.walletId, - network = token.network, - ) - } - - val initialTxFee = when (result) { - is Result.Success -> result.data - is Result.Failure -> raise(result.mapToFeeError()) - null -> raise(GetFeeError.UnknownError) - } + val initialTxFee = tokenFeeCalculator.estimateInitialFee( + userWallet = userWallet, + amount = amount, + txTokenCurrencyStatus = sendingTokenCurrencyStatus, + ).bind() val initialFeeEth = initialTxFee.normal as? Fee.Ethereum ?: raiseIllegalStateError( @@ -101,7 +81,7 @@ class EstimateFeeForTokenUseCase( tokenFeeCalculator.calculateTokenFee( walletManager = walletManager, - tokenForPayFeeStatus = tokenCurrencyStatus, + tokenForPayFeeStatus = feeTokenCurrencyStatus, nativeCurrencyStatus = nativeCurrencyStatus, initialFee = initialFeeEth, ).bind() @@ -126,15 +106,4 @@ class EstimateFeeForTokenUseCase( ?: raiseIllegalStateError("WalletManager type ${walletManager?.javaClass?.name} not supported") return ethereumWalletManager } - - private suspend fun demoTransactionSender( - userWallet: UserWallet, - cryptoCurrency: CryptoCurrency, - ): DemoTransactionSender { - return DemoTransactionSender( - walletManagersFacade - .getOrCreateWalletManager(userWallet.walletId, cryptoCurrency.network) - ?: error("WalletManager is null"), - ) - } } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/TokenFeeCalculator.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/TokenFeeCalculator.kt index c7d9a57ea7..803a21668b 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/TokenFeeCalculator.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/TokenFeeCalculator.kt @@ -61,11 +61,11 @@ internal class TokenFeeCalculator( suspend fun estimateInitialFee( userWallet: UserWallet, amount: BigDecimal, - tokenCurrencyStatus: CryptoCurrencyStatus, + txTokenCurrencyStatus: CryptoCurrencyStatus, ): Either { return either { - val network = tokenCurrencyStatus.currency.network - val amountData = amount.convertToSdkAmount(tokenCurrencyStatus) + val network = txTokenCurrencyStatus.currency.network + val amountData = amount.convertToSdkAmount(txTokenCurrencyStatus) val result = if (userWallet is UserWallet.Cold && demoConfig.isDemoCardId(userWallet.scanResponse.card.cardId) ) { diff --git a/domain/transaction/src/test/java/com/tangem/domain/transaction/usecase/gasless/TokenFeeCalculatorTest.kt b/domain/transaction/src/test/java/com/tangem/domain/transaction/usecase/gasless/TokenFeeCalculatorTest.kt index a5ba8d69b6..e3a91a642c 100644 --- a/domain/transaction/src/test/java/com/tangem/domain/transaction/usecase/gasless/TokenFeeCalculatorTest.kt +++ b/domain/transaction/src/test/java/com/tangem/domain/transaction/usecase/gasless/TokenFeeCalculatorTest.kt @@ -129,7 +129,7 @@ class TokenFeeCalculatorTest { val result = tokenFeeCalculator.estimateInitialFee( userWallet = mockUserWallet, amount = amount, - tokenCurrencyStatus = tokenStatus, + txTokenCurrencyStatus = tokenStatus, ) // Then @@ -154,7 +154,7 @@ class TokenFeeCalculatorTest { val result = tokenFeeCalculator.estimateInitialFee( userWallet = mockUserWallet, amount = amount, - tokenCurrencyStatus = tokenStatus, + txTokenCurrencyStatus = tokenStatus, ) // Then @@ -173,7 +173,7 @@ class TokenFeeCalculatorTest { val result = tokenFeeCalculator.estimateInitialFee( userWallet = mockUserWallet, amount = amount, - tokenCurrencyStatus = tokenStatus, + txTokenCurrencyStatus = tokenStatus, ) // Then diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt index dcbec88d76..a9a2f27737 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt @@ -14,6 +14,10 @@ class GetExplorerTransactionUrlUseCase( return either { catch( block = { + if (txHash.isEmpty()) { + raise(TxStatusError.EmptyUrlError) + } + repository.getTxExploreUrl(txHash, networkId).ifEmpty { raise(TxStatusError.EmptyUrlError) } diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt index 82527c66bf..d50adee6f6 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt @@ -186,6 +186,7 @@ sealed class CommonSendAnalyticEvents( enum class CommonSendSource(val analyticsName: String) { Send("Send"), + Swap("Swap"), SendWithSwap("Send&Swap"), WalletConnect("WalletConnect"), NFT("NFT"), diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt index 8a6a6edf3e..77877cf76e 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt @@ -37,6 +37,7 @@ sealed class FeeSelectorParams { override val feeDisplaySource: FeeDisplaySource, override val analyticsCategoryName: String, override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource, + val bottomSheetShown: (Boolean) -> Unit = {}, ) : FeeSelectorParams() data class FeeSelectorDetailsParams( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/FeeBlock.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/FeeBlockSuccess.kt similarity index 98% rename from features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/FeeBlock.kt rename to features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/FeeBlockSuccess.kt index 473a7b235e..c9db856ad1 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/FeeBlock.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/FeeBlockSuccess.kt @@ -19,7 +19,7 @@ import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.impl.R @Composable -internal fun FeeBlock(feeSelectorUM: FeeSelectorUM) { +fun FeeBlockSuccess(feeSelectorUM: FeeSelectorUM) { if (feeSelectorUM !is FeeSelectorUM.Content) return val feeExtraInfo = feeSelectorUM.feeExtraInfo val feeFiatRateUM = feeSelectorUM.feeFiatRateUM diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt index 5da1137ef5..baf95a222c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt @@ -65,6 +65,10 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor( ) init { + bottomSheetSlot.subscribe { + params.bottomSheetShown(it.child != null) + } + model.uiState .onEach { onResult(it) } .launchIn(componentScope) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorTokenSelectedTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorTokenSelectedTransformer.kt index bb7d3d5df7..f11d9c8dd9 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorTokenSelectedTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorTokenSelectedTransformer.kt @@ -13,6 +13,7 @@ class FeeSelectorTokenSelectedTransformer( override fun transform(prevState: FeeSelectorUM): FeeSelectorUM { return if (prevState is FeeSelectorUM.Content) { prevState.copy( + isPrimaryButtonEnabled = false, selectedFeeItem = FeeItem.Loading, feeItems = persistentListOf(FeeItem.Loading), feeExtraInfo = prevState.feeExtraInfo.copy( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt index 7d07f291bd..04905ad82d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt @@ -21,7 +21,7 @@ import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toPx import com.tangem.core.ui.utils.toTimeFormat import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent -import com.tangem.features.send.v2.common.ui.FeeBlock +import com.tangem.features.send.v2.common.ui.FeeBlockSuccess import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.ui.state.SendUM @@ -106,7 +106,7 @@ private fun SuccessContent( onClick = {}, ) destinationBlockComponent.Content(modifier = Modifier) - FeeBlock(feeSelectorUM = sendUM.feeSelectorUM) + FeeBlockSuccess(feeSelectorUM = sendUM.feeSelectorUM) SpacerH(16.dp) } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt index b5e13f0e06..2f18427779 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt @@ -21,7 +21,7 @@ import com.tangem.core.ui.utils.toPx import com.tangem.core.ui.utils.toTimeFormat import com.tangem.features.nft.component.NFTDetailsBlockComponent import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent -import com.tangem.features.send.v2.common.ui.FeeBlock +import com.tangem.features.send.v2.common.ui.FeeBlockSuccess import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM @@ -109,7 +109,7 @@ private fun SuccessContent( } nftDetailsBlockComponent.Content(modifier = Modifier) destinationBlockComponent.Content(modifier = Modifier) - FeeBlock(feeSelectorUM = nftSendUM.feeSelectorUM) + FeeBlockSuccess(feeSelectorUM = nftSendUM.feeSelectorUM) SpacerH(16.dp) } } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index 62e8af81b4..6ad374ecef 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -263,13 +263,14 @@ internal class SendWithSwapConfirmModel @Inject constructor( estimateFeeForTokenUseCase( amount = amountValue, userWallet = params.userWallet, - tokenCurrencyStatus = maybeToken, + feeTokenCurrencyStatus = maybeToken, + sendingTokenCurrencyStatus = primaryCurrencyStatus, ) } else { estimateFeeForGaslessTxUseCase( amount = amountValue, userWallet = params.userWallet, - cryptoCurrencyStatus = primaryCurrencyStatus, + sendingTokenCurrencyStatus = primaryCurrencyStatus, ) } } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 6f4381b8a6..d8ba2cc67b 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -1,10 +1,14 @@ package com.tangem.feature.swap.domain +import arrow.core.Either +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.express.models.ExpressOperationType import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.models.TransactionFeeExtended import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount import com.tangem.feature.swap.domain.models.domain.PermissionOptions @@ -37,7 +41,7 @@ interface SwapInteractor { * @param providers list of providers to find quote * @param amountToSwap amount you want to swap * @param reduceBalanceBy amount to reduce from balance (used for fee calculation) - * @param selectedFee selected fee to swap + * @param txFeeSealedState selected fee to swap * @return */ @Suppress("LongParameterList") @@ -50,7 +54,7 @@ interface SwapInteractor { providers: List, amountToSwap: String, reduceBalanceBy: BigDecimal, - selectedFee: FeeType = FeeType.NORMAL, + txFeeSealedState: TxFeeSealedState, ): Map /** @@ -132,6 +136,50 @@ interface SwapInteractor { averageDuration: Int? = null, ) + /** + * Loads fee for swap transaction + * + * @param fromToken token from which want to swap + * @param fromAccount account from which swap will be made + * @param toToken token that receive after swap + * @param toAccount account to which receive token after swap + * @param amount amount you want to swap + * @param reduceBalanceBy amount to reduce from balance (used for fee calculation) + * @param selectedFeeToken selected token to pay fee or null to pay fee with coin + */ + @Suppress("LongParameterList") + suspend fun loadFeeForSwapTransaction( + fromToken: CryptoCurrencyStatus, + fromAccount: Account.CryptoPortfolio?, + toToken: CryptoCurrencyStatus, + toAccount: Account.CryptoPortfolio?, + amount: String, + reduceBalanceBy: BigDecimal, + provider: SwapProvider, + selectedFeeToken: CryptoCurrencyStatus?, + ): Either + + /** + * Loads fee for swap transaction + * + * @param fromToken token from which want to swap + * @param fromAccount account from which swap will be made + * @param toToken token that receive after swap + * @param toAccount account to which receive token after swap + * @param amount amount you want to swap + * @param reduceBalanceBy amount to reduce from balance (used for fee calculation) + */ + @Suppress("LongParameterList") + suspend fun loadFeeForSwapTransaction( + fromToken: CryptoCurrencyStatus, + fromAccount: Account.CryptoPortfolio?, + toToken: CryptoCurrencyStatus, + toAccount: Account.CryptoPortfolio?, + amount: String, + reduceBalanceBy: BigDecimal, + provider: SwapProvider, + ): Either + interface Factory { fun create(selectedWalletId: UserWalletId): SwapInteractor } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 72a86eaec1..286471b0a0 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -3,6 +3,7 @@ package com.tangem.feature.swap.domain import android.util.Base64 import arrow.core.Either import arrow.core.getOrElse +import arrow.core.raise.either import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.solana.SolanaTransactionHelper import com.tangem.blockchain.common.Amount @@ -42,10 +43,14 @@ import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.models.TransactionFeeExtended import com.tangem.domain.transaction.usecase.* +import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase +import com.tangem.domain.transaction.usecase.gasless.EstimateFeeForGaslessTxUseCase +import com.tangem.domain.transaction.usecase.gasless.EstimateFeeForTokenUseCase +import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.usecase.GetUserWalletUseCase -import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.SwapAmount @@ -55,6 +60,7 @@ import com.tangem.feature.swap.domain.models.ui.* import com.tangem.lib.crypto.BlockchainUtils.SOLANA_TRANSACTION_SIZE_THRESHOLD_BYTES import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.ProxyAmount +import com.tangem.utils.coroutines.runSuspendCatching import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -87,6 +93,10 @@ internal class SwapInteractorImpl @AssistedInject constructor( private val initialToCurrencyResolver: InitialToCurrencyResolver, private val validateTransactionUseCase: ValidateTransactionUseCase, private val estimateFeeUseCase: EstimateFeeUseCase, + private val estimateFeeForTokenUseCase: EstimateFeeForTokenUseCase, + private val estimateFeeForGaslessTxUseCase: EstimateFeeForGaslessTxUseCase, + private val getFeeForTokenUseCase: GetFeeForTokenUseCase, + private val createAndSendGaslessTransactionUseCase: CreateAndSendGaslessTransactionUseCase, private val getFeeUseCase: GetFeeUseCase, private val getEthSpecificFeeUseCase: GetEthSpecificFeeUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, @@ -385,7 +395,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( providers: List, amountToSwap: String, reduceBalanceBy: BigDecimal, - selectedFee: FeeType, + txFeeSealedState: TxFeeSealedState, ): Map { Timber.i( """ @@ -394,7 +404,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( |- toToken: $toToken |- providers: $providers |- amountToSwap: $amountToSwap - |- selectedFee: $selectedFee + |- selectedFee: $txFeeSealedState """.trimIndent(), ) @@ -416,7 +426,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( toToken = toToken, toAccount = toAccount, provider = provider, - selectedFee = selectedFee, + txFeeSealedState = txFeeSealedState, amount = amount, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, expressOperationType = ExpressOperationType.SWAP, @@ -429,7 +439,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( toToken = toToken, toAccount = toAccount, provider = provider, - selectedFee = selectedFee, + txFeeSealedState = txFeeSealedState, amount = amount, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, expressOperationType = ExpressOperationType.SWAP, @@ -447,7 +457,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( amount = amount, reduceBalanceBy = reduceBalanceBy, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, - selectedFee = selectedFee, + txFeeSealedState = txFeeSealedState, ) } } @@ -462,7 +472,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( toToken: CryptoCurrencyStatus, toAccount: Account.CryptoPortfolio?, provider: SwapProvider, - selectedFee: FeeType, + txFeeSealedState: TxFeeSealedState, amount: SwapAmount, isBalanceWithoutFeeEnough: Boolean, expressOperationType: ExpressOperationType, @@ -517,7 +527,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( toToken = toToken, toAccount = toAccount, amount = amount, - selectedFee = selectedFee, + txFeeSealedState = txFeeSealedState, expressOperationType = expressOperationType, ) } else { @@ -532,10 +542,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( networkId = networkId, isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, - txFee = TxFeeState.Empty, - transactionFee = null, + txFeeSealedState = txFeeSealedState, includeFeeInAmount = IncludeFeeInAmount.Excluded, // exclude for dex - selectedFee = selectedFee, ) } } @@ -547,7 +555,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( toToken: CryptoCurrencyStatus, toAccount: Account.CryptoPortfolio?, provider: SwapProvider, - selectedFee: FeeType, + txFeeSealedState: TxFeeSealedState, amount: SwapAmount, isBalanceWithoutFeeEnough: Boolean, expressOperationType: ExpressOperationType, @@ -574,7 +582,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( toToken = toToken, toAccount = toAccount, amount = amount, - selectedFee = selectedFee, + txFeeSealedState = txFeeSealedState, expressOperationType = expressOperationType, ) } else { @@ -589,10 +597,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( networkId = networkId, isAllowedToSpend = true, isBalanceWithoutFeeEnough = false, - txFee = TxFeeState.Empty, - transactionFee = null, + txFeeSealedState = txFeeSealedState, includeFeeInAmount = IncludeFeeInAmount.Excluded, // exclude for dex - selectedFee = selectedFee, ) } } @@ -607,7 +613,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( amount: SwapAmount, reduceBalanceBy: BigDecimal, isBalanceWithoutFeeEnough: Boolean, - selectedFee: FeeType, + txFeeSealedState: TxFeeSealedState, ): Pair { return provider to loadCexQuoteData( networkId = networkId, @@ -620,22 +626,34 @@ internal class SwapInteractorImpl @AssistedInject constructor( isAllowedToSpend = true, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, provider = provider, - selectedFee = selectedFee, + txFeeSealedState = txFeeSealedState, ) } private suspend fun manageWarnings( fromTokenStatus: CryptoCurrencyStatus, amount: SwapAmount, - feeState: TxFeeState, - selectedFee: FeeType, + txFeeSealed: TxFeeSealedState?, includeFeeInAmount: IncludeFeeInAmount, ): CryptoCurrencyCheck { - val fee = when (feeState) { - TxFeeState.Empty -> BigDecimal.ZERO - is TxFeeState.MultipleFeeState -> feeState.getFeeByType(selectedFee).feeValue - is TxFeeState.SingleFeeState -> feeState.fee.feeValue - } + val fee = when (txFeeSealed) { + is TxFeeSealedState.Component -> { + if (txFeeSealed.txFee.selectedToken?.currency is CryptoCurrency.Token) { + BigDecimal.ZERO + } else { + txFeeSealed.txFee.fee.amount.value + } + } + is TxFeeSealedState.Legacy -> { + when (val feeState = txFeeSealed.txFeeState) { + TxFeeState.Empty -> BigDecimal.ZERO + is TxFeeState.MultipleFeeState -> feeState.getFeeByType(txFeeSealed.selectedFee).fee.amount.value + is TxFeeState.SingleFeeState -> feeState.fee.fee.amount.value + } + } + null -> BigDecimal.ZERO + } ?: BigDecimal.ZERO + val balanceAfterTransaction = getCoinBalanceAfterTransaction( fromTokenStatus = fromTokenStatus, amount = amount, @@ -703,7 +721,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun manageTransactionValidationWarnings( fromToken: CryptoCurrencyStatus, amount: SwapAmount, - feeState: TxFeeState, + txFeeSealedState: TxFeeSealedState, userWalletId: UserWalletId, ): Throwable? { val currency = fromToken.currency @@ -712,13 +730,20 @@ internal class SwapInteractorImpl @AssistedInject constructor( if (blockchain == Blockchain.Stellar) { return null } + val feeValue = when (txFeeSealedState) { + is TxFeeSealedState.Component -> txFeeSealedState.txFee.fee.amount.value + is TxFeeSealedState.Legacy -> { + when (val feeState = txFeeSealedState.txFeeState) { + TxFeeState.Empty -> BigDecimal.ZERO + is TxFeeState.MultipleFeeState -> feeState.normalFee.fee.amount.value + is TxFeeState.SingleFeeState -> feeState.fee.fee.amount.value + } + } + } + val fee = Fee.Common( amount = Amount( - value = when (feeState) { - TxFeeState.Empty -> BigDecimal.ZERO - is TxFeeState.MultipleFeeState -> feeState.normalFee.feeValue - is TxFeeState.SingleFeeState -> feeState.fee.feeValue - }, + value = feeValue, blockchain = blockchain, ), ) @@ -793,7 +818,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( if (isSolana(networkId)) { onSwapSolanaDex( provider = swapProvider, - networkId = currencyToSend.currency.network.backendId, swapData = requireNotNull(swapData), currencyToSendStatus = currencyToSend, currencyToGetStatus = currencyToGet, @@ -805,7 +829,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( if (fee == null) return SwapTransactionState.Error.UnknownError onSwapDex( provider = swapProvider, - networkId = currencyToSend.currency.network.backendId, swapData = requireNotNull(swapData), currencyToSendStatus = currencyToSend, currencyToGetStatus = currencyToGet, @@ -819,50 +842,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( } } - // override suspend fun updateQuotesStateWithSelectedFee( - // state: SwapState.QuotesLoadedState, - // selectedFee: FeeType, - // fromToken: CryptoCurrencyStatus, - // amountToSwap: String, - // reduceBalanceBy: BigDecimal, - // ): SwapState.QuotesLoadedState { - // val amountDecimal = toBigDecimalOrNull(amountToSwap) - // if (amountDecimal == null || amountDecimal.signum() == 0) { - // return state - // } - // val amount = SwapAmount(amountDecimal, fromToken.currency.decimals) - // val includeFeeInAmount = getIncludeFeeInAmount( - // networkId = fromToken.currency.network.backendId, - // txFee = state.txFee, - // amount = amount, - // reduceBalanceBy = reduceBalanceBy, - // fromToken = fromToken.currency, - // selectedFee = selectedFee, - // ) - // val fee = when (val txFee = state.txFee) { - // TxFeeState.Empty -> BigDecimal.ZERO - // is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeIncludeOtherNativeFee - // is TxFeeState.SingleFeeState -> txFee.fee.feeIncludeOtherNativeFee - // } - // val feeState = getFeeState( - // fee = fee, - // spendAmount = amount, - // networkId = fromToken.currency.network.backendId, - // fromTokenStatus = fromToken, - // ) - // return state.copy( - // permissionState = PermissionDataState.Empty, - // preparedSwapConfigState = state.preparedSwapConfigState.copy( - // feeState = feeState, - // isBalanceEnough = includeFeeInAmount !is IncludeFeeInAmount.BalanceNotEnough, - // includeFeeInAmount = includeFeeInAmount, - // ), - // ) - // } - private suspend fun onSwapDex( provider: SwapProvider, - networkId: String, swapData: SwapDataModel, currencyToSendStatus: CryptoCurrencyStatus, currencyToGetStatus: CryptoCurrencyStatus, @@ -874,7 +855,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" } val txValue = requireNotNull(swapData.transaction.txValue) { "txValue is null" } val amount = SwapAmount(amountDecimal, currencyToSendStatus.currency.decimals) - val derivationPath = currencyToSendStatus.currency.network.derivationPath.value val dexTransaction = swapData.transaction as ExpressTransactionModel.DEX val dataToSign = dexTransaction.txData val amountToSend = createNativeAmountForDex(txValue, currencyToSendStatus.currency.network) @@ -893,14 +873,12 @@ internal class SwapInteractorImpl @AssistedInject constructor( return handleSwapResult( provider = provider, - networkId = networkId, swapData = swapData, currencyToSendStatus = currencyToSendStatus, currencyToGetStatus = currencyToGetStatus, fromAccount = fromAccount, toAccount = toAccount, amount = amount, - derivationPath = derivationPath, txData = txData, payInAddress = getPayoutAddress(txData), ) @@ -908,7 +886,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun onSwapSolanaDex( provider: SwapProvider, - networkId: String, swapData: SwapDataModel, currencyToSendStatus: CryptoCurrencyStatus, currencyToGetStatus: CryptoCurrencyStatus, @@ -920,20 +897,17 @@ internal class SwapInteractorImpl @AssistedInject constructor( val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" } val txDataBase64 = requireNotNull(dexTransaction?.txData) { "txData is null" } val amount = SwapAmount(amountDecimal, currencyToSendStatus.currency.decimals) - val derivationPath = currencyToSendStatus.currency.network.derivationPath.value val compiledTransaction = TransactionData.Compiled( value = TransactionData.Compiled.Data.Bytes(Base64.decode(txDataBase64, Base64.NO_WRAP)), ) return handleSwapResult( provider = provider, - networkId = networkId, swapData = swapData, currencyToSendStatus = currencyToSendStatus, currencyToGetStatus = currencyToGetStatus, fromAccount = fromAccount, toAccount = toAccount, amount = amount, - derivationPath = derivationPath, txData = compiledTransaction, payInAddress = swapData.transaction.txTo, ) @@ -941,14 +915,12 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun handleSwapResult( provider: SwapProvider, - networkId: String, swapData: SwapDataModel, currencyToSendStatus: CryptoCurrencyStatus, currencyToGetStatus: CryptoCurrencyStatus, fromAccount: Account.CryptoPortfolio?, toAccount: Account.CryptoPortfolio?, amount: SwapAmount, - derivationPath: String?, txData: TransactionData, payInAddress: String, ): SwapTransactionState { @@ -995,7 +967,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( currencyToGetStatus.currency.symbol, ), toAmountValue = swapData.toTokenAmount.value, - txHash = userWalletManager.getLastTransactionHash(networkId, derivationPath).orEmpty(), + txHash = txHash, timestamp = System.currentTimeMillis(), ) }, @@ -1082,10 +1054,10 @@ internal class SwapInteractorImpl @AssistedInject constructor( if (userWallet is UserWallet.Cold && isDemoCardUseCase(userWallet.scanResponse.card.cardId)) { return SwapTransactionState.Error.UnknownError } - + val fee = requireNotNull(txFee) val txData = createTransferTransactionUseCase( amount = amount.value.convertToSdkAmount(currencyToSend), - fee = requireNotNull(txFee).fee, + fee = fee.fee, memo = exchangeDataCex.txExtraId, destination = exchangeDataCex.txTo, userWalletId = userWalletId, @@ -1099,13 +1071,33 @@ internal class SwapInteractorImpl @AssistedInject constructor( return SwapTransactionState.Error.UnknownError } - val result = sendTransactionUseCase( - txData = txData, - userWallet = userWallet, - network = currencyToSend.currency.network, - ) + val result = when (fee) { + is TxFee.FeeComponent -> { + if (fee.selectedToken?.currency is CryptoCurrency.Token && + fee.transactionFeeResult is TransactionFeeResult.LoadedExtended + ) { + createAndSendGaslessTransactionUseCase.invoke( + transactionData = txData, + userWallet = userWallet, + fee = fee.transactionFeeResult.fee, + ) + } else { + sendTransactionUseCase( + txData = txData, + userWallet = userWallet, + network = currencyToSend.currency.network, + ) + } + } + is TxFee.Legacy -> { + sendTransactionUseCase( + txData = txData, + userWallet = userWallet, + network = currencyToSend.currency.network, + ) + } + } - val derivationPath = currencyToSend.currency.network.derivationPath.value val cexNetworkAddress = currencyToSend.value.networkAddress val cexFromAddress = cexNetworkAddress?.defaultAddress?.value.orEmpty() return result.fold( @@ -1146,10 +1138,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( currencyToGet.currency.symbol, ), toAmountValue = exchangeData.toTokenAmount.value, - txHash = userWalletManager.getLastTransactionHash( - currencyToSend.currency.network.backendId, - derivationPath, - ).orEmpty(), + txHash = txHash, txExternalUrl = txExternalUrl, timestamp = timestamp, ) @@ -1194,6 +1183,113 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) } + @Suppress("LongParameterList") + override suspend fun loadFeeForSwapTransaction( + fromToken: CryptoCurrencyStatus, + fromAccount: Account.CryptoPortfolio?, + toToken: CryptoCurrencyStatus, + toAccount: Account.CryptoPortfolio?, + amount: String, + reduceBalanceBy: BigDecimal, + provider: SwapProvider, + selectedFeeToken: CryptoCurrencyStatus?, + ): Either = either { + when (provider.type) { + ExchangeProviderType.DEX, + ExchangeProviderType.DEX_BRIDGE, + -> raise(GetFeeError.GaslessError.NetworkIsNotSupported) + ExchangeProviderType.CEX -> { + val amountDecimal = toBigDecimalOrNull(amount) + if (amountDecimal == null || amountDecimal.signum() == 0) { + raise(GetFeeError.UnknownError) + } + + return if (selectedFeeToken != null) { + estimateFeeForTokenUseCase( + userWallet = userWallet, + feeTokenCurrencyStatus = selectedFeeToken, + sendingTokenCurrencyStatus = fromToken, + amount = amountDecimal, + ) + } else { + estimateFeeForGaslessTxUseCase( + amount = amountDecimal, + userWallet = userWallet, + sendingTokenCurrencyStatus = fromToken, + ) + } + } + } + } + + override suspend fun loadFeeForSwapTransaction( + fromToken: CryptoCurrencyStatus, + fromAccount: Account.CryptoPortfolio?, + toToken: CryptoCurrencyStatus, + toAccount: Account.CryptoPortfolio?, + amount: String, + reduceBalanceBy: BigDecimal, + provider: SwapProvider, + ): Either = either { + return when (provider.type) { + ExchangeProviderType.DEX, + ExchangeProviderType.DEX_BRIDGE, + -> { + val fromNetworkAddress = fromToken.value.networkAddress + val dexFromAddress = fromNetworkAddress?.defaultAddress?.value.orEmpty() + val toNetworkAddress = toToken.value.networkAddress + val dexToAddress = toNetworkAddress?.defaultAddress?.value.orEmpty() + val amountBigDecimal = toBigDecimalOrNull(amount) + if (amountBigDecimal == null || amountBigDecimal.signum() == 0) { + raise(GetFeeError.UnknownError) + } + val swapAmount = SwapAmount(amountBigDecimal, fromToken.currency.decimals) + + repository.getExchangeData( + userWallet = userWallet, + fromContractAddress = fromToken.currency.getContractAddress(), + fromNetwork = fromToken.currency.network.backendId, + toContractAddress = toToken.currency.getContractAddress(), + fromAddress = dexFromAddress, + toNetwork = toToken.currency.network.backendId, + fromAmount = swapAmount.toStringWithRightOffset(), + fromDecimals = swapAmount.decimals, + toDecimals = toToken.currency.decimals, + providerId = provider.providerId, + rateType = RateType.FLOAT, + toAddress = dexToAddress, + refundAddress = fromToken.value.networkAddress?.defaultAddress?.value, + expressOperationType = ExpressOperationType.SWAP, + ).map { swapData -> + val networkId = fromToken.currency.network.backendId + val transaction = swapData.transaction as ExpressTransactionModel.DEX + + loadFeeForDex( + networkId = networkId, + transaction = transaction, + fromToken = fromToken, + ).getOrElse { raise(GetFeeError.UnknownError) } + }.mapLeft { + GetFeeError.UnknownError + } + } + ExchangeProviderType.CEX -> { + val amountDecimal = toBigDecimalOrNull(amount) + if (amountDecimal == null || amountDecimal.signum() == 0) { + raise(GetFeeError.UnknownError) + } + + estimateFeeUseCase.invoke( + amount = amountDecimal, + userWallet = userWallet, + cryptoCurrencyStatus = fromToken, + ).map { + it.patchTransactionFeeForSwap(INCREASE_GAS_LIMIT_FOR_SEND) + } + } + } + } + private suspend fun storeLastCryptoCurrencyId(cryptoCurrency: CryptoCurrency) { swapTransactionRepository.storeLastSwappedCryptoCurrencyId( UserWalletId(userWalletManager.getWalletId()), @@ -1291,31 +1387,25 @@ internal class SwapInteractorImpl @AssistedInject constructor( provider: SwapProvider, isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, - selectedFee: FeeType, + txFeeSealedState: TxFeeSealedState, ): SwapState { val fromToken = fromTokenStatus.currency val toToken = toTokenStatus.currency return coroutineScope { - val txFeeResult = getUnhandledFee( - amount = amount.value, - userWallet = userWallet, - cryptoCurrencyStatus = fromTokenStatus, + val txFeeSealedStateUpdated = updateTxFeeStateIfNeededForCEX( + txFeeSealedState = txFeeSealedState, + amount = amount, + fromTokenStatus = fromTokenStatus, ) - val txFee = if (provider.type == ExchangeProviderType.CEX) { - getFeeForCex(txFeeResult, fromTokenStatus) - } else { - TxFeeState.Empty - } - val includeFeeInAmount = getIncludeFeeInAmount( networkId = networkId, - txFee = txFee, amount = amount, reduceBalanceBy = reduceBalanceBy, - fromToken = fromToken, - selectedFee = selectedFee, + fromToken = fromTokenStatus, + txFeeSealedState = txFeeSealedStateUpdated, ) + val amountToRequest = if (includeFeeInAmount is IncludeFeeInAmount.Included) { includeFeeInAmount.amountSubtractFee } else { @@ -1346,14 +1436,39 @@ internal class SwapInteractorImpl @AssistedInject constructor( networkId = networkId, isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, - txFee = txFee, - transactionFee = txFeeResult.getOrNull(), + txFeeSealedState = txFeeSealedState, includeFeeInAmount = includeFeeInAmount, - selectedFee = selectedFee, ) } } + private suspend fun updateTxFeeStateIfNeededForCEX( + txFeeSealedState: TxFeeSealedState, + amount: SwapAmount, + fromTokenStatus: CryptoCurrencyStatus, + ): TxFeeSealedState { + return when (txFeeSealedState) { + is TxFeeSealedState.Component -> txFeeSealedState + is TxFeeSealedState.Legacy -> { + if (txFeeSealedState.txFeeState is TxFeeState.Empty) { + val txFeeResult = estimateFeeUseCase( + amount = amount.value, + userWallet = userWallet, + cryptoCurrencyStatus = fromTokenStatus, + ) + val txFee = getFeeForCex(txFeeResult, fromTokenStatus) + + TxFeeSealedState.Legacy( + txFeeState = txFee, + selectedFee = txFeeSealedState.selectedFee, + ) + } else { + txFeeSealedState + } + } + } + } + @Suppress("LongMethod") private suspend fun getQuotesState( provider: SwapProvider, @@ -1366,10 +1481,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( networkId: String, isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, - txFee: TxFeeState, - transactionFee: TransactionFee?, + txFeeSealedState: TxFeeSealedState, includeFeeInAmount: IncludeFeeInAmount, - selectedFee: FeeType, ): SwapState { return quoteDataModel.fold( ifRight = { quoteModel -> @@ -1382,23 +1495,35 @@ internal class SwapInteractorImpl @AssistedInject constructor( fromTokenAmount = amount, toTokenAmount = quoteModel.toTokenAmount, swapData = null, - txFeeState = txFee, + txFeeSealedState = txFeeSealedState, provider = provider, ).copy( currencyCheck = manageWarnings( fromTokenStatus = fromToken, amount = amount, - feeState = txFee, - selectedFee = selectedFee, + txFeeSealed = txFeeSealedState, includeFeeInAmount = includeFeeInAmount, ), validationResult = manageTransactionValidationWarnings( fromToken = fromToken, amount = amount, - feeState = txFee, + txFeeSealedState = txFeeSealedState, userWalletId = userWalletId, ), - minAdaValue = (transactionFee?.normal as? Fee.CardanoToken)?.minAdaValue, + minAdaValue = when (txFeeSealedState) { + is TxFeeSealedState.Component -> { + (txFeeSealedState.txFee.fee as? Fee.CardanoToken)?.minAdaValue + } + is TxFeeSealedState.Legacy -> { + when (txFeeSealedState.txFeeState) { + TxFeeState.Empty -> null + is TxFeeState.MultipleFeeState -> + (txFeeSealedState.txFeeState.normalFee.fee as? Fee.CardanoToken)?.minAdaValue + is TxFeeState.SingleFeeState -> + (txFeeSealedState.txFeeState.fee.fee as? Fee.CardanoToken)?.minAdaValue + } + } + }, ) when (provider.type) { @@ -1423,11 +1548,17 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) } ExchangeProviderType.CEX -> { - val fee = when (txFee) { - TxFeeState.Empty -> BigDecimal.ZERO - is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeValue - is TxFeeState.SingleFeeState -> txFee.fee.feeValue + val fee = when (txFeeSealedState) { + is TxFeeSealedState.Component -> txFeeSealedState.txFee.fee.amount.value + is TxFeeSealedState.Legacy -> { + when (val txFee = txFeeSealedState.txFeeState) { + TxFeeState.Empty -> BigDecimal.ZERO + is TxFeeState.MultipleFeeState -> txFee.priorityFee.fee.amount.value + is TxFeeState.SingleFeeState -> txFee.fee.fee.amount.value + } + } } + val feeState = getFeeState( fee = fee, spendAmount = amount, @@ -1477,20 +1608,76 @@ internal class SwapInteractorImpl @AssistedInject constructor( return SwapState.SwapError(fromTokenSwapInfo, expressDataError, includeFeeInAmount) } - @Suppress("CyclomaticComplexMethod") + @Suppress("CyclomaticComplexMethod", "NestedBlockDepth", "CastNullableToNonNullableType") private suspend fun getIncludeFeeInAmount( networkId: String, - txFee: TxFeeState, + amount: SwapAmount, + reduceBalanceBy: BigDecimal, + fromToken: CryptoCurrencyStatus, + txFeeSealedState: TxFeeSealedState, + ): IncludeFeeInAmount { + return when (txFeeSealedState) { + is TxFeeSealedState.Component -> { + if (fromToken.currency.id == txFeeSealedState.txFee.selectedToken?.currency?.id) { + val fee = txFeeSealedState.txFee.fee.amount.value ?: BigDecimal.ZERO + if (txFeeSealedState.txFee.selectedToken.currency is CryptoCurrency.Coin) { + getIncludeFeeInAmountForNative( + networkId = networkId, + amount = amount, + reduceBalanceBy = reduceBalanceBy, + fromToken = fromToken.currency, + feeValue = fee, + ) + } else { + // we have a token selected for fee payment the same as sending token + val reducedBalance = fromToken.value.amount as BigDecimal - reduceBalanceBy + when { + amount.value > reducedBalance -> IncludeFeeInAmount.BalanceNotEnough + amount.value + fee <= reducedBalance -> IncludeFeeInAmount.Excluded + else -> { + if (fee < amount.value) { + IncludeFeeInAmount.Included( + amountSubtractFee = SwapAmount( + value = reducedBalance - fee, + decimals = fromToken.currency.decimals, + ), + ) + } else { + IncludeFeeInAmount.Excluded + } + } + } + } + } else { + IncludeFeeInAmount.Excluded + } + } + is TxFeeSealedState.Legacy -> { + val feeValue = when (val txFee = txFeeSealedState.txFeeState) { + TxFeeState.Empty -> BigDecimal.ZERO + is TxFeeState.MultipleFeeState -> txFee.getFeeByType( + txFeeSealedState.selectedFee, + ).feeIncludeOtherNativeFee + is TxFeeState.SingleFeeState -> txFee.fee.feeIncludeOtherNativeFee + } + getIncludeFeeInAmountForNative( + networkId = networkId, + amount = amount, + reduceBalanceBy = reduceBalanceBy, + fromToken = fromToken.currency, + feeValue = feeValue, + ) + } + } + } + + private suspend fun getIncludeFeeInAmountForNative( + networkId: String, amount: SwapAmount, reduceBalanceBy: BigDecimal, fromToken: CryptoCurrency, - selectedFee: FeeType, + feeValue: BigDecimal, ): IncludeFeeInAmount { - val feeValue = when (txFee) { - TxFeeState.Empty -> BigDecimal.ZERO - is TxFeeState.MultipleFeeState -> txFee.getFeeByType(selectedFee).feeIncludeOtherNativeFee - is TxFeeState.SingleFeeState -> txFee.fee.feeIncludeOtherNativeFee - } val feePaidCurrency = getFeePaidCurrency( currency = fromToken, ) @@ -1590,7 +1777,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( toToken: CryptoCurrencyStatus, toAccount: Account.CryptoPortfolio?, amount: SwapAmount, - selectedFee: FeeType, + txFeeSealedState: TxFeeSealedState, expressOperationType: ExpressOperationType, ): SwapState { val fromNetworkAddress = fromToken.value.networkAddress @@ -1621,37 +1808,28 @@ internal class SwapInteractorImpl @AssistedInject constructor( ?.movePointLeft(nativeCoinDecimals) ?: BigDecimal.ZERO - val txFeeState = if (isSolana(networkId)) { - val transactionBytes = Base64.decode(transaction.txData, Base64.NO_WRAP) - - val formattedHash = getFormattedHash(transactionBytes) - - if (formattedHash.size > SOLANA_TRANSACTION_SIZE_THRESHOLD_BYTES && userWallet is UserWallet.Cold) { - return produceDexSwapDataError( - error = ExpressDataError.TooLargeSolanaTransactionError, - fromToken = fromToken, - fromAccount = fromAccount, - amount = amount, - ) - } - - getFeeDataForSolanaDexSwap( - network = fromToken.currency.network, - transactionBytes = transactionBytes, + val txFeeState = loadFeeForDex( + networkId = networkId, + transaction = transaction, + fromToken = fromToken, + ).getOrElse { error -> + return@fold produceDexSwapDataError( + error = error, + fromToken = fromToken, + fromAccount = fromAccount, + amount = amount, ) - .toTxFeeState(fromToken.currency, otherNativeFee) - } else { - getFeeDataForDexSwap( - network = fromToken.currency.network, - transaction = transaction, - fromToken = fromToken.currency, - ) - .patchTransactionFeeForSwap(INCREASE_GAS_LIMIT_FOR_DEX) - .toTxFeeState(fromToken.currency, otherNativeFee) - } + }.toTxFeeState(fromToken.currency, otherNativeFee) val includeFeeInAmount = IncludeFeeInAmount.Excluded // exclude for dex - val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = txFeeState) + val feeByPriority = when (txFeeSealedState) { + is TxFeeSealedState.Component -> { + txFeeSealedState.txFee.fee.amount.value ?: BigDecimal.ZERO + } + is TxFeeSealedState.Legacy -> { + selectFeeByType(feeType = txFeeSealedState.selectedFee, txFeeState = txFeeState) + } + } val feeToCheckFunds = feeByPriority + (otherNativeFee ?: BigDecimal.ZERO) val isBalanceIncludeFeeEnough = isBalanceEnough(fromToken, amount, feeToCheckFunds) val feeState = getFeeState( @@ -1676,7 +1854,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( fromTokenAmount = amount, toTokenAmount = swapData.toTokenAmount, swapData = swapData, - txFeeState = txFeeState, + txFeeSealedState = txFeeSealedState, provider = provider, ) swapState.copy( @@ -1684,14 +1862,13 @@ internal class SwapInteractorImpl @AssistedInject constructor( currencyCheck = manageWarnings( fromTokenStatus = fromToken, amount = amount, - feeState = txFeeState, - selectedFee = selectedFee, + txFeeSealed = txFeeSealedState, includeFeeInAmount = includeFeeInAmount, ), validationResult = manageTransactionValidationWarnings( fromToken = fromToken, amount = amount, - feeState = txFeeState, + txFeeSealedState = txFeeSealedState, userWalletId = userWalletId, ), preparedSwapConfigState = preparedSwapConfigState, @@ -1708,6 +1885,36 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) } + private suspend fun loadFeeForDex( + networkId: String, + transaction: ExpressTransactionModel.DEX, + fromToken: CryptoCurrencyStatus, + ): Either = either { + if (isSolana(networkId)) { + val transactionBytes = Base64.decode(transaction.txData, Base64.NO_WRAP) + + val formattedHash = getFormattedHash(transactionBytes) + + if (formattedHash.size > SOLANA_TRANSACTION_SIZE_THRESHOLD_BYTES && userWallet is UserWallet.Cold) { + raise(ExpressDataError.TooLargeSolanaTransactionError) + } + + getFeeDataForSolanaDexSwap( + network = fromToken.currency.network, + transactionBytes = transactionBytes, + ) + } else { + getFeeDataForDexSwap( + network = fromToken.currency.network, + transaction = transaction, + fromToken = fromToken.currency, + ).map { fee -> + (fee as TransactionFeeResult.Loaded).fee + .patchTransactionFeeForSwap(INCREASE_GAS_LIMIT_FOR_DEX) + }.bind() + } + } + private suspend fun produceDexSwapDataError( error: ExpressDataError, fromToken: CryptoCurrencyStatus, @@ -1729,18 +1936,27 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) } + @Suppress("CyclomaticComplexMethod") private suspend fun getFeeDataForDexSwap( network: Network, transaction: ExpressTransactionModel.DEX, fromToken: CryptoCurrency, - ): TransactionFee { - return try { + selectedToken: CryptoCurrencyStatus? = null, + ): Either = either { + val nativeBalance = userWalletManager.getNativeTokenBalance( + networkId = network.backendId, + derivationPath = fromToken.network.derivationPath.value, + ) ?: ProxyAmount.empty() + + // if native balance is zero - we can't calculate fee + if (nativeBalance.value.signum() == 0) { + raise(ExpressDataError.UnknownError) + } + + try { val txAmountValue = transaction.txValue ?: error("unable to get txValue") - val nativeBalance = userWalletManager.getNativeTokenBalance( - networkId = network.backendId, - derivationPath = fromToken.network.derivationPath.value, - ) ?: ProxyAmount.empty() val amountToSend = createNativeAmountForDex(txAmountValue, fromToken.network) + // transaction.txValue is always native coin if (nativeBalance.value < amountToSend.value) { error("It's impossible to calculate fee for nativeBalance.value < amountToSend.value") @@ -1758,17 +1974,27 @@ internal class SwapInteractorImpl @AssistedInject constructor( sourceAddress = transaction.txFrom, extras = extras, ) - getFeeUseCase( - transactionData = transactionData, - network = network, - userWallet = userWallet, - ).getOrNull() ?: error("unable to calculate fee") + if (selectedToken != null && selectedToken.currency is CryptoCurrency.Token) { + getFeeForTokenUseCase( + transactionData = transactionData, + token = selectedToken.currency, + userWallet = userWallet, + ).getOrNull()?.let { TransactionFeeResult.LoadedExtended(it) } + ?: error("unable to calculate fee for token") + } else { + getFeeUseCase( + transactionData = transactionData, + network = network, + userWallet = userWallet, + ).getOrNull()?.let { TransactionFeeResult.Loaded(it) } ?: error("unable to calculate fee") + } } catch (_: IllegalStateException) { getEthSpecificFeeUseCase( userWallet = userWallet, cryptoCurrency = fromToken, gasLimit = transaction.gas, - ).getOrNull() ?: error("can't get fee for getEthSpecificFeeUseCase") + ).getOrNull()?.let { TransactionFeeResult.Loaded(it) } + ?: error("can't get fee for getEthSpecificFeeUseCase") } } @@ -1784,7 +2010,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( ).getOrNull() ?: error("unable to calculate fee") } - @Suppress("LongParameterList") + @Suppress("LongParameterList", "MaxChainedCallsOnSameLine") private suspend fun updateBalances( provider: SwapProvider, networkId: String, @@ -1795,7 +2021,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( fromTokenAmount: SwapAmount, toTokenAmount: SwapAmount, swapData: SwapDataModel?, - txFeeState: TxFeeState, + txFeeSealedState: TxFeeSealedState, ): SwapState.QuotesLoadedState { val fromToken = fromTokenStatus.currency val toToken = toTokenStatus.currency @@ -1823,8 +2049,18 @@ internal class SwapInteractorImpl @AssistedInject constructor( toRate = rates[toToken.id]?.toDouble() ?: 0.0, ), swapDataModel = swapData, - txFee = txFeeState, swapProvider = provider, + txFee = when (txFeeSealedState) { + is TxFeeSealedState.Component -> { + when (txFeeSealedState.txFee.transactionFeeResult) { + is TransactionFeeResult.Loaded -> + txFeeSealedState.txFee.transactionFeeResult.fee.toTxFeeState(fromToken, null) + is TransactionFeeResult.LoadedExtended -> + txFeeSealedState.txFee.transactionFeeResult.fee.transactionFee.toTxFeeState(fromToken, null) + } + } + is TxFeeSealedState.Legacy -> txFeeSealedState.txFeeState + }, minAdaValue = null, ) } @@ -1843,18 +2079,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) ?: TxFeeState.Empty } - private suspend fun getUnhandledFee( - amount: BigDecimal, - userWallet: UserWallet, - cryptoCurrencyStatus: CryptoCurrencyStatus, - ): Either { - return estimateFeeUseCase( - amount = amount, - userWallet = userWallet, - cryptoCurrencyStatus = cryptoCurrencyStatus, - ) - } - @Suppress("LongParameterList", "LongMethod") private suspend fun updatePermissionState( networkId: String, @@ -1929,8 +2153,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( val fee = when (feeState) { TxFeeState.Empty -> BigDecimal.ZERO - is TxFeeState.MultipleFeeState -> feeState.normalFee.feeValue - is TxFeeState.SingleFeeState -> feeState.fee.feeValue + is TxFeeState.MultipleFeeState -> feeState.normalFee.fee.amount.value + is TxFeeState.SingleFeeState -> feeState.fee.fee.amount.value } val swapFeeState = getFeeState( fee = fee, @@ -1994,7 +2218,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) // endregion TxFeeState.MultipleFeeState( - normalFee = TxFee( + normalFee = TxFee.Legacy( feeValue = feeNormal, feeFiatFormatted = normalFiatValue, feeCryptoFormatted = normalCryptoFee, @@ -2005,7 +2229,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( feeType = FeeType.NORMAL, fee = this.normal, ), - priorityFee = TxFee( + priorityFee = TxFee.Legacy( feeValue = feePriority, feeFiatFormatted = priorityFiatValue, feeCryptoFormatted = priorityCryptoFee, @@ -2035,7 +2259,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) // endregion TxFeeState.SingleFeeState( - fee = TxFee( + fee = TxFee.Legacy( feeValue = this.normal.amount.value ?: BigDecimal.ZERO, feeFiatFormatted = normalFiatValue, feeCryptoFormatted = normalCryptoFee, @@ -2145,12 +2369,12 @@ internal class SwapInteractorImpl @AssistedInject constructor( private fun selectFeeByType(feeType: FeeType, txFeeState: TxFeeState): BigDecimal { return when (txFeeState) { TxFeeState.Empty -> BigDecimal.ZERO - is TxFeeState.SingleFeeState -> txFeeState.fee.feeValue + is TxFeeState.SingleFeeState -> txFeeState.fee.fee.amount.value is TxFeeState.MultipleFeeState -> when (feeType) { - FeeType.NORMAL -> txFeeState.normalFee.feeValue - FeeType.PRIORITY -> txFeeState.priorityFee.feeValue + FeeType.NORMAL -> txFeeState.normalFee.fee.amount.value + FeeType.PRIORITY -> txFeeState.priorityFee.fee.amount.value } - } + } ?: BigDecimal.ZERO } private suspend fun isBalanceEnough( @@ -2378,4 +2602,24 @@ internal class SwapInteractorImpl @AssistedInject constructor( interface Factory : SwapInteractor.Factory { override fun create(selectedWalletId: UserWalletId): SwapInteractorImpl } +} + +sealed class TxFeeSealedState { + class Legacy(val txFeeState: TxFeeState, val selectedFee: FeeType) : TxFeeSealedState() + class Component(val txFee: TxFee.FeeComponent) : TxFeeSealedState() + + fun getTxFeeStateOrNull() = when (this) { + is Component -> null + is Legacy -> txFeeState + } +} + +sealed class TransactionFeeResult { + class Loaded(val fee: TransactionFee) : TransactionFeeResult() + class LoadedExtended(val fee: TransactionFeeExtended) : TransactionFeeResult() + + companion object { + fun from(fee: TransactionFee) = Loaded(fee) + fun from(fee: TransactionFeeExtended) = LoadedExtended(fee) + } } \ No newline at end of file diff --git a/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt similarity index 87% rename from features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt index 7473ad49ae..fedf814baa 100644 --- a/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt @@ -6,7 +6,12 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.swap.domain.models.ExpressDataError -import com.tangem.feature.swap.domain.models.domain.* +import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel +import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo +import com.tangem.feature.swap.domain.models.domain.PairsWithProviders +import com.tangem.feature.swap.domain.models.domain.QuoteModel +import com.tangem.feature.swap.domain.models.domain.RateType +import com.tangem.feature.swap.domain.models.domain.SwapDataModel import java.math.BigDecimal interface SwapRepository { diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ExpressDataError.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ExpressDataError.kt similarity index 100% rename from features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ExpressDataError.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ExpressDataError.kt diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ExpressException.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ExpressException.kt similarity index 100% rename from features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ExpressException.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ExpressException.kt diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/SwapAmount.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/SwapAmount.kt similarity index 100% rename from features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/SwapAmount.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/SwapAmount.kt diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt similarity index 100% rename from features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt similarity index 100% rename from features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/LeastTokenInfo.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/LeastTokenInfo.kt similarity index 100% rename from features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/LeastTokenInfo.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/LeastTokenInfo.kt diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/NetworkInfo.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/NetworkInfo.kt similarity index 100% rename from features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/NetworkInfo.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/NetworkInfo.kt diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/PairsWithProviders.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PairsWithProviders.kt similarity index 100% rename from features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/PairsWithProviders.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PairsWithProviders.kt diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/PermissionOptions.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PermissionOptions.kt similarity index 100% rename from features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/PermissionOptions.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PermissionOptions.kt diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/PreparedSwapConfigState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PreparedSwapConfigState.kt similarity index 100% rename from features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/PreparedSwapConfigState.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PreparedSwapConfigState.kt diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/QuoteModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/QuoteModel.kt similarity index 100% rename from features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/QuoteModel.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/QuoteModel.kt diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedLastSwappedCryptoCurrency.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedLastSwappedCryptoCurrency.kt similarity index 100% rename from features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedLastSwappedCryptoCurrency.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedLastSwappedCryptoCurrency.kt diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt similarity index 100% rename from features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapApproveType.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapApproveType.kt similarity index 100% rename from features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapApproveType.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapApproveType.kt diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapDataModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapDataModel.kt similarity index 100% rename from features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapDataModel.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapDataModel.kt diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapFeeState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapFeeState.kt similarity index 100% rename from features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapFeeState.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapFeeState.kt diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt similarity index 100% rename from features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/AmountFormatter.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/AmountFormatter.kt similarity index 100% rename from features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/AmountFormatter.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/AmountFormatter.kt diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt similarity index 80% rename from features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index 7c76557067..c735c821d4 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -4,6 +4,7 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck +import com.tangem.feature.swap.domain.TransactionFeeResult import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* @@ -91,11 +92,11 @@ data class RequestApproveStateData( sealed class TxFeeState { data class MultipleFeeState( - val normalFee: TxFee, - val priorityFee: TxFee, + val normalFee: TxFee.Legacy, + val priorityFee: TxFee.Legacy, ) : TxFeeState() { - fun getFeeByType(feeType: FeeType): TxFee { + fun getFeeByType(feeType: FeeType): TxFee.Legacy { return when (feeType) { FeeType.NORMAL -> normalFee FeeType.PRIORITY -> priorityFee @@ -104,23 +105,33 @@ sealed class TxFeeState { } data class SingleFeeState( - val fee: TxFee, + val fee: TxFee.Legacy, ) : TxFeeState() data object Empty : TxFeeState() } -data class TxFee( - val feeValue: BigDecimal, - val feeFiatFormatted: String, - val feeCryptoFormatted: String, - val feeIncludeOtherNativeFee: BigDecimal, - val feeFiatFormattedWithNative: String, - val feeCryptoFormattedWithNative: String, - val cryptoSymbol: String, - val feeType: FeeType, - val fee: Fee, -) +sealed class TxFee { + abstract val fee: Fee + + data class FeeComponent( + override val fee: Fee, + val transactionFeeResult: TransactionFeeResult, + val selectedToken: CryptoCurrencyStatus?, + ) : TxFee() + + data class Legacy( + val feeValue: BigDecimal, + val feeFiatFormatted: String, + val feeCryptoFormatted: String, + val feeIncludeOtherNativeFee: BigDecimal, + val feeFiatFormattedWithNative: String, + val feeCryptoFormattedWithNative: String, + val cryptoSymbol: String, + val feeType: FeeType, + override val fee: Fee, + ) : TxFee() +} enum class FeeType { NORMAL, PRIORITY diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt similarity index 100% rename from features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt similarity index 100% rename from features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt diff --git a/features/swap/impl/build.gradle.kts b/features/swap/impl/build.gradle.kts index ebb3536b3e..c31fc810fd 100644 --- a/features/swap/impl/build.gradle.kts +++ b/features/swap/impl/build.gradle.kts @@ -58,6 +58,8 @@ dependencies { implementation(projects.features.swap.domain.models) implementation(projects.features.wallet.api) implementation(projects.features.swap.api) + implementation(projects.features.sendV2.api) + implementation(projects.features.sendV2.impl) /** AndroidX */ implementation(deps.androidx.activity.compose) @@ -87,6 +89,10 @@ dependencies { implementation(deps.kotlin.serialization) implementation(deps.kotlin.immutable.collections) implementation(deps.timber) + implementation(deps.decompose.ext.compose) + + /** Tangem libs */ + implementation(tangemDeps.blockchain) /** DI */ implementation(deps.hilt.android) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt index 65c5f62d82..b391c87b53 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt @@ -3,17 +3,31 @@ package com.tangem.feature.swap import androidx.compose.animation.Crossfade import androidx.compose.foundation.background import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss import com.arkivanov.essenty.lifecycle.subscribe import com.tangem.common.ui.swapStoriesScreen.SwapStoriesScreen import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent import com.tangem.feature.swap.model.SwapModel +import com.tangem.feature.swap.models.SwapCardState.SwapCardData import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.ui.SwapScreen import com.tangem.feature.swap.ui.SwapSelectTokenScreen import com.tangem.feature.swap.ui.SwapSuccessScreen +import com.tangem.features.send.v2.api.SendFeatureToggles +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.swap.SwapComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -22,7 +36,9 @@ import dagger.assisted.AssistedInject @Suppress("UnusedPrivateMember") internal class DefaultSwapComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, - @Assisted params: SwapComponent.Params, + @Assisted private val params: SwapComponent.Params, + private val swapFeeSelectorBlockComponentFactory: SwapFeeSelectorBlockComponent.Factory, + private val sendFeatureToggles: SendFeatureToggles, ) : SwapComponent, AppComponentContext by appComponentContext { private val model: SwapModel = getOrCreateModel(params) @@ -34,8 +50,71 @@ internal class DefaultSwapComponent @AssistedInject constructor( ) } + val slotNavigation = SlotNavigation() + val childSlot = childSlot( + source = slotNavigation, + serializer = null, + childFactory = { config, context -> + createSwapFeeSelectorBlockComponent( + context = childByContext(context), + config = config, + ) + }, + ) + + private fun createSwapFeeSelectorBlockComponent( + context: AppComponentContext, + config: FeeSelectorConfig, + ): SwapFeeSelectorBlockComponent { + return swapFeeSelectorBlockComponentFactory.create( + context = context, + params = SwapFeeSelectorBlockComponent.Params( + repository = model.feeSelectorRepository, + userWalletId = params.userWalletId, + sendingCryptoCurrencyStatus = config.sendingCurrencyStatus, + feeCryptoCurrencyStatus = config.feeCurrencyStatus, + analyticsParams = SwapFeeSelectorBlockComponent.AnalyticsParams( + analyticsCategoryName = CommonSendAnalyticEvents.SEND_CATEGORY, + analyticsSendSource = CommonSendAnalyticEvents.CommonSendSource.Swap, + ), + ), + ) + } + + data class FeeSelectorConfig( + val sendingCurrencyStatus: CryptoCurrencyStatus, + val feeCurrencyStatus: CryptoCurrencyStatus, + ) + + @Suppress("LongMethod") @Composable override fun Content(modifier: Modifier) { + if (sendFeatureToggles.isGaslessTransactionsEnabled) { + val sendCardData = model.uiState.sendCardData as? SwapCardData + val feePaidCryptoCurrency = model.dataState.feePaidCryptoCurrency + LaunchedEffect(sendCardData?.token?.currency, feePaidCryptoCurrency?.currency) { + val sendingCryptoCurrencyStatus = sendCardData?.token ?: run { + slotNavigation.dismiss() + return@LaunchedEffect + } + + val feeCurrencyStatus = feePaidCryptoCurrency ?: run { + slotNavigation.dismiss() + return@LaunchedEffect + } + + slotNavigation.activate( + FeeSelectorConfig( + sendingCurrencyStatus = sendingCryptoCurrencyStatus, + feeCurrencyStatus = feeCurrencyStatus, + ), + ) + } + } + + val feeSelectorChildStackState by childSlot.subscribeAsState() + val feeSelectorBlockComponent = feeSelectorChildStackState.child?.instance + Crossfade( modifier = Modifier.background(TangemTheme.colors.background.secondary), targetState = model.currentScreen, @@ -47,16 +126,30 @@ internal class DefaultSwapComponent @AssistedInject constructor( if (storiesConfig != null) { SwapStoriesScreen(config = storiesConfig) } else { - SwapScreen(stateHolder = model.uiState) + SwapScreen( + stateHolder = model.uiState, + feeSelectorBlockComponent = feeSelectorBlockComponent, + ) } } - SwapNavScreen.Main -> SwapScreen(stateHolder = model.uiState) + SwapNavScreen.Main -> SwapScreen( + stateHolder = model.uiState, + feeSelectorBlockComponent = feeSelectorBlockComponent, + ) SwapNavScreen.Success -> { val successState = model.uiState.successState + val feeSelectorState by model.feeSelectorRepository.state.collectAsStateWithLifecycle() if (successState != null) { - SwapSuccessScreen(state = successState, model.uiState.onBackClicked) + SwapSuccessScreen( + state = successState, + feeSelectorUM = feeSelectorState, + onBack = model.uiState.onBackClicked, + ) } else { - SwapScreen(stateHolder = model.uiState) + SwapScreen( + stateHolder = model.uiState, + feeSelectorBlockComponent = feeSelectorBlockComponent, + ) } } SwapNavScreen.SelectToken -> { @@ -64,7 +157,10 @@ internal class DefaultSwapComponent @AssistedInject constructor( if (tokenState != null) { SwapSelectTokenScreen(state = tokenState, onBack = model.uiState.onBackClicked) } else { - SwapScreen(stateHolder = model.uiState) + SwapScreen( + stateHolder = model.uiState, + feeSelectorBlockComponent = feeSelectorBlockComponent, + ) } } } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/component/SwapFeeSelectorBlockComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/component/SwapFeeSelectorBlockComponent.kt new file mode 100644 index 0000000000..19e1760238 --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/component/SwapFeeSelectorBlockComponent.kt @@ -0,0 +1,99 @@ +package com.tangem.feature.swap.component + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import arrow.core.Either +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.child +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.models.TransactionFeeExtended +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.params.FeeSelectorParams +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +class SwapFeeSelectorBlockComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: Params, + feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + private val feeSelectorBlockComponent = + feeSelectorBlockComponentFactory.create( + context = child("swapFeeSelectorBlock"), + params = FeeSelectorParams.FeeSelectorBlockParams( + state = params.repository.state.value, + userWalletId = params.userWalletId, + onLoadFee = params.repository::loadFee, + onLoadFeeExtended = if (params.repository is ModelRepositoryExtended) { + params.repository::loadFeeExtended + } else { + null + }, + feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen, + feeStateConfiguration = FeeSelectorParams.FeeStateConfiguration.ExcludeLow, + feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, + cryptoCurrencyStatus = params.sendingCryptoCurrencyStatus, + analyticsCategoryName = params.analyticsParams.analyticsCategoryName, + analyticsSendSource = params.analyticsParams.analyticsSendSource, + bottomSheetShown = params.repository::choosingInProgress, + ), + onResult = params.repository::onResult, + ) + + init { + params.repository.state + .onEach(feeSelectorBlockComponent::updateState) + .launchIn(componentScope) + } + + @Composable + override fun Content(modifier: Modifier) { + feeSelectorBlockComponent.Content(modifier = modifier) + } + + interface ModelRepository { + val state: StateFlow + get() = MutableStateFlow(FeeSelectorUM.Loading) + + fun onResult(newState: FeeSelectorUM) + + suspend fun loadFee(): Either + + fun choosingInProgress(updatedState: Boolean) + } + + interface ModelRepositoryExtended : ModelRepository { + suspend fun loadFeeExtended( + selectedToken: CryptoCurrencyStatus? = null, + ): Either + } + + class AnalyticsParams( + val analyticsCategoryName: String, + val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource, + ) + + class Params( + val userWalletId: UserWalletId, + val sendingCryptoCurrencyStatus: CryptoCurrencyStatus, + val feeCryptoCurrencyStatus: CryptoCurrencyStatus, + val analyticsParams: AnalyticsParams, + val repository: ModelRepository, + ) + + @AssistedFactory + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index b887bb2de7..adc9b652e3 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import arrow.core.Either import arrow.core.getOrElse +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.routing.AppRouter import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState.InProgress.getApproveTypeOrNull @@ -52,17 +53,23 @@ import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.models.TransactionFeeExtended import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.swap.analytics.StoriesEvents import com.tangem.feature.swap.analytics.SwapEvents +import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent import com.tangem.feature.swap.domain.SwapInteractor +import com.tangem.feature.swap.domain.TransactionFeeResult +import com.tangem.feature.swap.domain.TxFeeSealedState import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.ExpressException 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 com.tangem.feature.swap.models.SwapCardState import com.tangem.feature.swap.models.SwapStateHolder import com.tangem.feature.swap.models.UiActions import com.tangem.feature.swap.models.market.SwapMarketsListBatchFlowManager @@ -73,6 +80,9 @@ import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter import com.tangem.feature.swap.ui.StateBuilder import com.tangem.feature.swap.utils.formatToUIRepresentation +import com.tangem.features.send.v2.api.SendFeatureToggles +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger import com.tangem.features.swap.SwapComponent import com.tangem.features.swap.SwapFeatureToggles import com.tangem.utils.Provider @@ -124,6 +134,8 @@ internal class SwapModel @Inject constructor( private val getTangemPayCurrencyStatusUseCase: GetTangemPayCurrencyStatusUseCase, private val tangemPayWithdrawUseCase: TangemPayWithdrawUseCase, private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork, + private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger, + private val sendFeatureToggles: SendFeatureToggles, private val getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, private val swapFeatureToggles: SwapFeatureToggles, ) : Model() { @@ -169,7 +181,7 @@ internal class SwapModel @Inject constructor( private val searchDebouncer = Debouncer() private val singleTaskScheduler = SingleTaskScheduler>() - private var dataState by mutableStateOf(SwapProcessDataState()) + var dataState by mutableStateOf(SwapProcessDataState()) var uiState: SwapStateHolder by mutableStateOf( stateBuilder.createInitialLoadingState( @@ -180,6 +192,8 @@ internal class SwapModel @Inject constructor( ) private set + val feeSelectorRepository = FeeSelectorRepository() + // shows currency order (direct - swap initial to selected, reversed = selected to initial) private var isOrderReversed = false private val lastAmount = mutableStateOf(INITIAL_AMOUNT) @@ -359,6 +373,7 @@ internal class SwapModel @Inject constructor( analyticsEventHandler.send(SwapEvents.ChooseTokenScreenOpened(hasAvailableTokens = isAnyAvailableTokens)) } + @Suppress("LongMethod") private fun initTokens(isReverseFromTo: Boolean) { modelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { @@ -394,6 +409,21 @@ internal class SwapModel @Inject constructor( isReverseFromTo = isReverseFromTo, ) + val fromCryptoCurrency = if (isOrderReversed) { + dataState.toCryptoCurrency + } else { + dataState.fromCryptoCurrency + } + + fromCryptoCurrency?.let { cryptoCurrency -> + dataState = dataState.copy( + feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( + userWalletId = userWalletId, + cryptoCurrencyStatus = cryptoCurrency, + ).getOrNull(), + ) + } + (dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let { coin -> subscribeToCoinBalanceUpdates( userWalletId = userWalletId, @@ -524,6 +554,7 @@ internal class SwapModel @Inject constructor( reduceBalanceBy: BigDecimal, toProvidersList: List, isSilent: Boolean = false, + updateFeeBlock: Boolean = true, ) { singleTaskScheduler.cancelTask() if (!isSilent) { @@ -535,6 +566,7 @@ internal class SwapModel @Inject constructor( toAccount = toAccount, mainTokenId = initialCurrencyFrom.id.value, ) + feeSelectorRepository.state.value = FeeSelectorUM.Loading } singleTaskScheduler.scheduleTask( modelScope, @@ -546,11 +578,12 @@ internal class SwapModel @Inject constructor( amount = amount, reduceBalanceBy = reduceBalanceBy, toProvidersList = toProvidersList, + updateFeeBlock = updateFeeBlock, ), ) } - private fun startLoadingQuotesFromLastState(isSilent: Boolean = false) { + private fun startLoadingQuotesFromLastState(isSilent: Boolean = false, updateFeeBlock: Boolean = true) { val fromCurrency = dataState.fromCryptoCurrency val toCurrency = dataState.toCryptoCurrency val amount = dataState.amount @@ -564,6 +597,7 @@ internal class SwapModel @Inject constructor( isSilent = isSilent, reduceBalanceBy = dataState.reduceBalanceBy, toProvidersList = findSwapProviders(fromCurrency, toCurrency), + updateFeeBlock = updateFeeBlock, ) } } @@ -576,6 +610,7 @@ internal class SwapModel @Inject constructor( amount: String, reduceBalanceBy: BigDecimal, toProvidersList: List, + updateFeeBlock: Boolean = true, ): PeriodicTask> { return PeriodicTask( delay = UPDATE_DELAY, @@ -596,7 +631,7 @@ internal class SwapModel @Inject constructor( providers = toProvidersList, amountToSwap = amount, reduceBalanceBy = reduceBalanceBy, - selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL, + txFeeSealedState = getSelectedFeeState(), ) } }, @@ -612,12 +647,17 @@ internal class SwapModel @Inject constructor( tokenSwapInfoForProviders = successStates.entries .associate { it.key.providerId to it.value.toTokenInfo }, ) + if (updateFeeBlock) { + modelScope.launch { feeSelectorReloadTrigger.triggerUpdate() } + } } else { + feeSelectorRepository.state.value = FeeSelectorUM.Error(GetFeeError.UnknownError) Timber.e("Accidentally empty quotes list") } }, onError = { error -> Timber.e("Error when loading quotes: $error") + feeSelectorRepository.state.value = FeeSelectorUM.Error(GetFeeError.UnknownError) uiState = stateBuilder.addNotification(uiState, null) { startLoadingQuotesFromLastState() } }, ) @@ -657,7 +697,7 @@ internal class SwapModel @Inject constructor( swapProvider = provider, bestRatedProviderId = bestRatedProviderId, isNeedBestRateBadge = dataState.lastLoadedSwapStates.consideredProvidersStates().size > 1, - selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL, + selectedFeeType = (getSelectedFee() as? TxFee.Legacy)?.feeType ?: FeeType.NORMAL, isReverseSwapPossible = isReverseSwapPossible(), needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), hideFee = tangemPayInput?.isWithdrawal == true, @@ -799,8 +839,8 @@ internal class SwapModel @Inject constructor( } } - private fun updateOrSelectFee(state: SwapState.QuotesLoadedState): TxFee? { - val selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL + private fun updateOrSelectFee(state: SwapState.QuotesLoadedState): TxFee.Legacy? { + val selectedFeeType = (getSelectedFee() as? TxFee.Legacy)?.feeType ?: FeeType.NORMAL return when (val txFee = state.txFee) { TxFeeState.Empty -> null is TxFeeState.MultipleFeeState -> { @@ -827,7 +867,7 @@ internal class SwapModel @Inject constructor( return } val fromCurrency = requireNotNull(dataState.fromCryptoCurrency) - val fee = dataState.selectedFee + val fee = getSelectedFee() if (fee == null && tangemPayInput?.isWithdrawal != true) { makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) @@ -855,7 +895,10 @@ internal class SwapModel @Inject constructor( makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) return@onSuccess } - sendSuccessSwapEvent(fromCurrency.currency, fee.feeType) + sendSuccessSwapEvent( + fromCurrency.currency, + (getSelectedFee() as? TxFee.Legacy)?.feeType ?: FeeType.NORMAL, + ) val url = getExplorerTransactionUrlUseCase( txHash = swapTransactionState.txHash, networkId = fromCurrency.currency.network.id, @@ -967,7 +1010,7 @@ internal class SwapModel @Inject constructor( private fun sendSuccessEvent() { val provider = dataState.selectedProvider ?: return - val fee = dataState.selectedFee?.feeType ?: return + val fee = (getSelectedFee() as? TxFee.Legacy)?.feeType ?: FeeType.NORMAL val fromCurrency = dataState.fromCryptoCurrency?.currency ?: return val toCurrency = dataState.toCryptoCurrency?.currency ?: return val fromDerivationIndex = dataState.fromAccount?.derivationIndex?.value @@ -1027,6 +1070,7 @@ internal class SwapModel @Inject constructor( }.onSuccess { swapTransactionState -> when (swapTransactionState) { is SwapTransactionState.TxSent -> { + // TODO [REDACTED_TASK_KEY] gasless analytics sendApproveSuccessEvent(fromToken, feeForPermission.feeType, approveType) updateWalletBalance() uiState = stateBuilder.loadingPermissionState(uiState) @@ -1242,12 +1286,14 @@ internal class SwapModel @Inject constructor( .onEach { (account, currencyStatus) -> Timber.d("${coin.id} balance is ${currencyStatus.value.amount ?: "null"}") - dataState = dataState.copy( - feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( - userWalletId = userWalletId, - cryptoCurrencyStatus = currencyStatus, - ).getOrNull() ?: currencyStatus, - ) + if (isFromCurrency) { + dataState = dataState.copy( + feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( + userWalletId = userWalletId, + cryptoCurrencyStatus = currencyStatus, + ).getOrNull() ?: currencyStatus, + ) + } uiState = when { isFromCurrency && currencyStatus.currency.id == dataState.fromCryptoCurrency?.currency?.id -> { @@ -1280,12 +1326,14 @@ internal class SwapModel @Inject constructor( .onEach { status -> Timber.d("${coin.id} balance is ${status.value.amount ?: "null"}") - dataState = dataState.copy( - feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( - userWalletId = userWalletId, - cryptoCurrencyStatus = status, - ).getOrNull() ?: status, - ) + if (isFromCurrency) { + dataState = dataState.copy( + feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( + userWalletId = userWalletId, + cryptoCurrencyStatus = status, + ).getOrNull() ?: status, + ) + } uiState = when { isFromCurrency && status.currency.id == dataState.fromCryptoCurrency?.currency?.id -> { @@ -1492,7 +1540,7 @@ internal class SwapModel @Inject constructor( uiState = stateBuilder.updateApproveType(uiState, approveType) }, onClickFee = { - val selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL + val selectedFee = (getSelectedFee() as? TxFee.Legacy)?.feeType ?: FeeType.NORMAL val txFeeState = dataState.getCurrentLoadedSwapState()?.txFee as? TxFeeState.MultipleFeeState ?: return@UiActions uiState = stateBuilder.showSelectFeeBottomSheet( @@ -1503,9 +1551,9 @@ internal class SwapModel @Inject constructor( uiState = stateBuilder.dismissBottomSheet(uiState) } }, - onSelectFeeType = { feeType -> + onSelectFeeType = { txFee -> uiState = stateBuilder.dismissBottomSheet(uiState) - dataState = dataState.copy(selectedFee = feeType) + dataState = dataState.copy(selectedFee = txFee) modelScope.launch(dispatchers.io) { startLoadingQuotesFromLastState(false) } @@ -1527,6 +1575,10 @@ internal class SwapModel @Inject constructor( val swapState = dataState.lastLoadedSwapStates[provider] val fromToken = dataState.fromCryptoCurrency if (provider != null && swapState != null && fromToken != null) { + modelScope.launch { + feeSelectorRepository.state.value = FeeSelectorUM.Loading + feeSelectorReloadTrigger.triggerUpdate() + } analyticsEventHandler.send(SwapEvents.ProviderChosen(provider)) uiState = stateBuilder.dismissBottomSheet(uiState) setupLoadedState( @@ -1860,7 +1912,11 @@ internal class SwapModel @Inject constructor( destinationAddress = transaction?.txTo.orEmpty(), tokenSymbol = fromCurrencyStatus.currency.symbol, amount = dataState.amount.orEmpty(), - fee = dataState.selectedFee?.feeCryptoFormatted.orEmpty(), + fee = when (val fee = getSelectedFee()) { + is TxFee.FeeComponent -> fee.fee.amount.value?.toString() + is TxFee.Legacy -> fee.feeCryptoFormatted + null -> "" + }, ), ) @@ -1900,6 +1956,137 @@ internal class SwapModel @Inject constructor( } } + private fun getSelectedFeeState(): TxFeeSealedState { + if (!sendFeatureToggles.isGaslessTransactionsEnabled) { + return TxFeeSealedState.Legacy( + txFeeState = TxFeeState.Empty, + selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL, + ) + } + + val feeStateUM = feeSelectorRepository.state.value as? FeeSelectorUM.Content + ?: return TxFeeSealedState.Legacy( + txFeeState = TxFeeState.Empty, + selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL, + ) + + val transactionFeeExtended = feeStateUM.feeExtraInfo.transactionFeeExtended + return TxFeeSealedState.Component( + txFee = TxFee.FeeComponent( + transactionFeeResult = transactionFeeExtended?.let { TransactionFeeResult.from(it) } + ?: TransactionFeeResult.from(feeStateUM.fees), + fee = feeStateUM.selectedFeeItem.fee, + selectedToken = feeStateUM.feeExtraInfo.feeCryptoCurrencyStatus, + ), + ) + } + + private fun getSelectedFee(): TxFee? { + if (!sendFeatureToggles.isGaslessTransactionsEnabled) { + return dataState.selectedFee + } + + val feeStateUM = feeSelectorRepository.state.value as? FeeSelectorUM.Content ?: return null + val transactionFeeExtended = feeStateUM.feeExtraInfo.transactionFeeExtended + + return TxFee.FeeComponent( + transactionFeeResult = transactionFeeExtended?.let { TransactionFeeResult.from(it) } + ?: TransactionFeeResult.from(feeStateUM.fees), + fee = feeStateUM.selectedFeeItem.fee, + selectedToken = feeStateUM.feeExtraInfo.feeCryptoCurrencyStatus, + ) + } + + inner class FeeSelectorRepository : SwapFeeSelectorBlockComponent.ModelRepositoryExtended { + + override val state = MutableStateFlow(FeeSelectorUM.Loading) + + override suspend fun loadFeeExtended( + selectedToken: CryptoCurrencyStatus?, + ): Either { + val sendCardData = + uiState.sendCardData as? SwapCardState.SwapCardData ?: return Either.Left(GetFeeError.UnknownError) + val receiveCardData = + uiState.receiveCardData as? SwapCardState.SwapCardData ?: return Either.Left(GetFeeError.UnknownError) + val fromToken = sendCardData.token ?: return Either.Left(GetFeeError.UnknownError) + val toToken = receiveCardData.token ?: return Either.Left(GetFeeError.UnknownError) + val selectedProvider = dataState.selectedProvider ?: return Either.Left(GetFeeError.UnknownError) + + if (dataState.lastLoadedSwapStates[selectedProvider] !is SwapState.QuotesLoadedState) { + return Either.Left(GetFeeError.UnknownError) + } + + return swapInteractor.loadFeeForSwapTransaction( + fromToken = fromToken, + fromAccount = dataState.fromAccount, + toToken = toToken, + toAccount = dataState.toAccount, + provider = selectedProvider, + amount = lastAmount.value, + reduceBalanceBy = lastReducedBalanceBy.value, + selectedFeeToken = selectedToken, + ) + } + + override fun onResult(newState: FeeSelectorUM) { + state.value = newState + + // If fee currency is same as from currency, we need to reload quotes to update fee info + if (newState is FeeSelectorUM.Content && + dataState.fromCryptoCurrency?.currency?.id == newState.feeExtraInfo.feeCryptoCurrencyStatus.currency.id + ) { + // block swap button until fee is loaded + uiState = uiState.copy( + swapButton = uiState.swapButton.copy( + isEnabled = false, + ), + ) + modelScope.launch { + startLoadingQuotesFromLastState( + isSilent = true, + updateFeeBlock = false, + ) + } + } + } + + override suspend fun loadFee(): Either { + val sendCardData = + uiState.sendCardData as? SwapCardState.SwapCardData ?: return Either.Left(GetFeeError.UnknownError) + val receiveCardData = + uiState.receiveCardData as? SwapCardState.SwapCardData ?: return Either.Left(GetFeeError.UnknownError) + val fromToken = sendCardData.token ?: return Either.Left(GetFeeError.UnknownError) + val toToken = receiveCardData.token ?: return Either.Left(GetFeeError.UnknownError) + val selectedProvider = dataState.selectedProvider ?: return Either.Left(GetFeeError.UnknownError) + + if (dataState.lastLoadedSwapStates[selectedProvider] !is SwapState.QuotesLoadedState) { + return Either.Left(GetFeeError.UnknownError) + } + + return swapInteractor.loadFeeForSwapTransaction( + fromToken = fromToken, + fromAccount = dataState.fromAccount, + toToken = toToken, + toAccount = dataState.toAccount, + provider = selectedProvider, + amount = lastAmount.value, + reduceBalanceBy = lastReducedBalanceBy.value, + ) + } + + override fun choosingInProgress(updatedState: Boolean) { + // We shouldn't load quotes while user is choosing fee + if (updatedState) { + singleTaskScheduler.cancelTask() + } else { + startLoadingQuotesFromLastState( + isSilent = true, + updateFeeBlock = false, + ) + } + } + } + private companion object { const val INITIAL_AMOUNT = "" const val UPDATE_DELAY = 10000L diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt index eafe9d8abb..f7a1817cb3 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt @@ -15,6 +15,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.SwapAmount +import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount import com.tangem.feature.swap.domain.models.domain.SwapFeeState import com.tangem.feature.swap.domain.models.ui.* @@ -164,7 +165,7 @@ internal class SwapNotificationsFactory( addExistentialWarningNotification( existentialDeposit = quoteModel.currencyCheck?.existentialDeposit, - feeAmount = fee?.feeValue.orZero(), + feeAmount = fee?.fee?.amount?.value.orZero(), sendingAmount = amountToRequest.value, cryptoCurrencyStatus = fromCurrencyStatus, onReduceClick = { reduceBy, reduceByDiff, _ -> @@ -187,7 +188,7 @@ internal class SwapNotificationsFactory( if (!isCardano) { addDustWarningNotification( dustValue = quoteModel.currencyCheck?.dustValue, - feeValue = fee?.feeValue.orZero(), + feeValue = fee?.fee?.amount?.value.orZero(), sendingAmount = amountToRequest.value, cryptoCurrencyStatus = fromCurrencyStatus, feeCurrencyStatus = feeCryptoCurrencyStatus, @@ -275,7 +276,7 @@ internal class SwapNotificationsFactory( } } - private fun selectFeeByType(feeType: FeeType, txFeeState: TxFeeState): TxFee? { + private fun selectFeeByType(feeType: FeeType, txFeeState: TxFeeState): TxFee.Legacy? { return when (txFeeState) { TxFeeState.Empty -> null is TxFeeState.SingleFeeState -> txFeeState.fee @@ -296,7 +297,9 @@ internal class SwapNotificationsFactory( val shouldShowCoverWarning = quoteModel.preparedSwapConfigState.isBalanceEnough && quoteModel.permissionState !is PermissionDataState.PermissionLoading && feeEnoughState.feeCurrency != fromToken - val isGaslessAvailable = iGaslessFeeSupportedForNetwork(fromToken.network) + + val isGaslessAvailable = iGaslessFeeSupportedForNetwork(fromToken.network) && + quoteModel.swapProvider.type == ExchangeProviderType.CEX if (shouldShowCoverWarning && !isGaslessAvailable) { add( SwapNotificationUM.Error.UnableToCoverFeeWarning( diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapProcessDataState.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapProcessDataState.kt index d18df192c1..f352427c56 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapProcessDataState.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapProcessDataState.kt @@ -22,7 +22,7 @@ data class SwapProcessDataState( val reduceBalanceBy: BigDecimal = BigDecimal.ZERO, val approveDataModel: RequestApproveStateData? = null, val swapDataModel: SwapDataModel? = null, - val selectedFee: TxFee? = null, + val selectedFee: TxFee.Legacy? = null, val tokensDataState: TokensDataStateExpress? = null, val selectedProvider: SwapProvider? = null, val lastLoadedSwapStates: Map = emptyMap(), diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt index 5156ce3e1e..1d50867cc6 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt @@ -7,7 +7,7 @@ import com.tangem.core.ui.extensions.TextReference data class SwapSuccessStateHolder( val timestamp: Long, val txUrl: String, - val fee: TextReference, + val fee: TextReference?, val rate: TextReference, val shouldShowStatusButton: Boolean, val providerName: TextReference, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt index 25c2225451..7330e3ed67 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -24,7 +24,7 @@ data class UiActions( val onStoriesClose: (Int) -> Unit, val onRetryClick: () -> Unit, val onClickFee: () -> Unit, - val onSelectFeeType: (TxFee) -> Unit, + val onSelectFeeType: (TxFee.Legacy) -> Unit, val onProviderClick: (String) -> Unit, val onProviderSelect: (String) -> Unit, val onBuyClick: (CryptoCurrency) -> Unit, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index fcf5220ed6..89570857d7 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -826,7 +826,6 @@ internal class StateBuilder( onStatusClick: () -> Unit, txUrl: String, ): SwapStateHolder { - val fee = requireNotNull(dataState.selectedFee) val fromCryptoCurrency = requireNotNull(dataState.fromCryptoCurrency) val toCryptoCurrency = requireNotNull(dataState.toCryptoCurrency) val fromAmount = swapTransactionState.fromAmountValue ?: BigDecimal.ZERO @@ -846,7 +845,9 @@ internal class StateBuilder( shouldShowStatusButton = shouldShowStatus, providerIcon = providerState.iconUrl, rate = providerState.subtitle, - fee = stringReference("${fee.feeCryptoFormattedWithNative} (${fee.feeFiatFormattedWithNative})"), + fee = dataState.selectedFee?.let { fee -> + stringReference("${fee.feeCryptoFormattedWithNative} (${fee.feeFiatFormattedWithNative})") + }, fromTitle = getFromCardAccountTitle(fromAccount = dataState.fromAccount), toTitle = getToCardAccountTitle(toAccount = dataState.toAccount), fromTokenAmount = stringReference(swapTransactionState.fromAmount.orEmpty()), diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt index e474858740..78ad44f4f9 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt @@ -1,24 +1,27 @@ package com.tangem.feature.swap.ui import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import com.tangem.common.ui.bottomsheet.permission.GiveTxPermissionBottomSheet import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.WindowInsetsZero +import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent 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.presentation.R @Composable -internal fun SwapScreen(stateHolder: SwapStateHolder) { +internal fun SwapScreen(stateHolder: SwapStateHolder, feeSelectorBlockComponent: SwapFeeSelectorBlockComponent?) { BackHandler(onBack = stateHolder.onBackClicked) Scaffold( @@ -36,6 +39,17 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) { SwapScreenContent( state = stateHolder, + feeBlock = if (feeSelectorBlockComponent != null) { + @Composable { modifier: Modifier -> + feeSelectorBlockComponent.Content( + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), + ) + } + } else { + null + }, modifier = Modifier.padding(scaffoldPaddings), ) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 3aa2c4d059..1c6eca0df3 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -49,7 +49,11 @@ import kotlinx.collections.immutable.persistentListOf @Suppress("LongMethod") @Composable -internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modifier) { +internal fun SwapScreenContent( + state: SwapStateHolder, + modifier: Modifier = Modifier, + feeBlock: @Composable ((Modifier) -> Unit)? = null, +) { val keyboard by keyboardAsState() Box( @@ -74,7 +78,11 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi ProviderItemBlock(state = state.providerState) - FeeItemBlock(state = state.fee) + if (feeBlock != null) { + feeBlock(Modifier.fillMaxWidth()) + } else { + FeeItemBlock(state = state.fee) + } if (state.notifications.isNotEmpty()) SwapNotifications(notifications = state.notifications) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt index 2d51569af0..9caf68f4c0 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt @@ -29,14 +29,16 @@ import com.tangem.core.ui.utils.toTimeFormat import com.tangem.feature.swap.models.SwapSuccessStateHolder import com.tangem.feature.swap.presentation.R import com.tangem.feature.swap.preview.SwapSuccessStatePreview +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.common.ui.FeeBlockSuccess @Composable -fun SwapSuccessScreen(state: SwapSuccessStateHolder, onBack: () -> Unit) { +fun SwapSuccessScreen(state: SwapSuccessStateHolder, feeSelectorUM: FeeSelectorUM?, onBack: () -> Unit) { Scaffold( modifier = Modifier.systemBarsPadding(), containerColor = TangemTheme.colors.background.secondary, content = { padding -> - SwapSuccessScreenContent(padding = padding, state = state) + SwapSuccessScreenContent(padding = padding, feeSelectorUM = feeSelectorUM, state = state) }, topBar = { AppBarWithBackButton( @@ -58,7 +60,11 @@ fun SwapSuccessScreen(state: SwapSuccessStateHolder, onBack: () -> Unit) { } @Composable -private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: PaddingValues) { +private fun SwapSuccessScreenContent( + state: SwapSuccessStateHolder, + feeSelectorUM: FeeSelectorUM?, + padding: PaddingValues, +) { Column( modifier = Modifier .fillMaxSize() @@ -101,7 +107,10 @@ private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: Pad .background(TangemTheme.colors.background.action), ) SpacerH16() - if (state.fee != TextReference.EMPTY) { + + if (feeSelectorUM != null) { + FeeBlockSuccess(feeSelectorUM) + } else if (state.fee != null && state.fee != TextReference.EMPTY) { InputRowDefault( title = TextReference.Res(R.string.common_network_fee_title), text = state.fee, @@ -205,7 +214,7 @@ private fun SwapSuccessScreenButtons( @Composable private fun Preview_Success() { TangemThemePreview { - SwapSuccessScreen(SwapSuccessStatePreview.state) {} + SwapSuccessScreen(SwapSuccessStatePreview.state, null) {} } } // endregion preview \ No newline at end of file