diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapDirection.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapDirection.kt index 9c60cc2ec0..4f51ce02f1 100644 --- a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapDirection.kt +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapDirection.kt @@ -13,4 +13,12 @@ import com.tangem.domain.swap.models.SwapDirection.Reverse enum class SwapDirection { Direct, Reverse, + ; + + companion object { + inline fun SwapDirection.withSwapDirection(onDirect: () -> T, onReverse: () -> T): T = when (this) { + Direct -> onDirect() + Reverse -> onReverse() + } + } } \ No newline at end of file diff --git a/features/swap-v2/impl/build.gradle.kts b/features/swap-v2/impl/build.gradle.kts index 817039a569..8c880b498a 100644 --- a/features/swap-v2/impl/build.gradle.kts +++ b/features/swap-v2/impl/build.gradle.kts @@ -34,6 +34,9 @@ dependencies { implementation(projects.common.ui) implementation(projects.common.routing) + /** Libs */ + implementation(projects.libs.crypto) + implementation(tangemDeps.blockchain) { exclude(module = "joda-time") } @@ -54,6 +57,9 @@ dependencies { implementation(projects.domain.transaction.models) implementation(projects.domain.transaction) implementation(projects.domain.legacy) + implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.balanceHiding) + implementation(projects.domain.settings) /** Compose */ implementation(deps.compose.foundation) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapUtils.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapUtils.kt new file mode 100644 index 0000000000..01da434698 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapUtils.kt @@ -0,0 +1,6 @@ +package com.tangem.features.swap.v2.impl.common + +internal object SwapUtils { + const val INCREASE_GAS_LIMIT_FOR_DEX = 112 // 12% + const val INCREASE_GAS_LIMIT_FOR_CEX = 105 // 5% +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt new file mode 100644 index 0000000000..7e3af10fa2 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt @@ -0,0 +1,24 @@ +package com.tangem.features.swap.v2.impl.common.entity + +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed class ConfirmUM { + + abstract val isPrimaryButtonEnabled: Boolean + + data class Content( + override val isPrimaryButtonEnabled: Boolean = false, + val isTransactionInProcess: Boolean, + val showTapHelp: Boolean, + val sendingFooter: TextReference, + val notifications: ImmutableList, + ) : ConfirmUM() + + data object Empty : ConfirmUM() { + override val isPrimaryButtonEnabled: Boolean = false + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/SendWithSwapRoute.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/SendWithSwapRoute.kt new file mode 100644 index 0000000000..3f0b9b70be --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/SendWithSwapRoute.kt @@ -0,0 +1,31 @@ +package com.tangem.features.swap.v2.impl.sendviaswap + +import com.tangem.core.decompose.navigation.Route +import com.tangem.features.send.v2.api.subcomponents.destination.DestinationRoute +import com.tangem.features.swap.v2.impl.amount.SwapAmountRoute +import kotlinx.serialization.Serializable + +internal sealed class SendWithSwapRoute : Route { + + abstract val isEditMode: Boolean + + @Serializable + data class Amount( + override val isEditMode: Boolean, + ) : SendWithSwapRoute(), SwapAmountRoute + + @Serializable + data class Destination( + override val isEditMode: Boolean, + ) : SendWithSwapRoute(), DestinationRoute + + @Serializable + data object Confirm : SendWithSwapRoute() { + override val isEditMode: Boolean = false + } + + @Serializable + data object Success : SendWithSwapRoute() { + override val isEditMode: Boolean = false + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt new file mode 100644 index 0000000000..c32952b36c --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt @@ -0,0 +1,162 @@ +package com.tangem.features.swap.v2.impl.sendviaswap.confirm + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.child +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent +import com.tangem.features.send.v2.api.SendNotificationsComponent +import com.tangem.features.send.v2.api.entity.PredefinedValues +import com.tangem.features.send.v2.api.params.FeeSelectorParams +import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent +import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams +import com.tangem.features.swap.v2.impl.amount.SwapAmountBlockComponent +import com.tangem.features.swap.v2.impl.amount.SwapAmountComponentParams +import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM +import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute +import com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.SendWithSwapConfirmModel +import com.tangem.features.swap.v2.impl.sendviaswap.confirm.ui.SendWithSwapConfirmContent +import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.* +import java.math.BigDecimal + +internal class SendWithSwapConfirmComponent @AssistedInject constructor( + @Assisted private val appComponentContext: AppComponentContext, + @Assisted private val params: Params, + sendDestinationBlockComponent: SendDestinationBlockComponent.Factory, + feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, + sendNotificationsComponentFactory: SendNotificationsComponent.Factory, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + private val model: SendWithSwapConfirmModel = getOrCreateModel(params = params) + + private val blockClickEnableFlow = MutableStateFlow(false) + + private val amountBlockComponent = SwapAmountBlockComponent( + appComponentContext = child("sendWithSwapConfirmAmountBlock"), + params = SwapAmountComponentParams.AmountBlockParams( + amountUM = model.uiState.value.amountUM, + analyticsCategoryName = params.analyticsCategoryName, + userWallet = params.userWallet, + blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), + primaryCryptoCurrencyStatusFlow = params.primaryCryptoCurrencyStatusFlow, + secondaryCryptoCurrency = model.secondaryCurrency, + isBalanceHidingFlow = params.isBalanceHidingFlow, + swapDirection = params.swapDirection, + ), + onResult = model::onAmountResult, + onClick = model::showEditAmount, + ) + + private val sendDestinationBlockComponent = sendDestinationBlockComponent.create( + context = child("sendWithSwapConfirmDestinationBlock"), + params = SendDestinationComponentParams.DestinationBlockParams( + state = model.uiState.value.destinationUM, + analyticsCategoryName = params.analyticsCategoryName, + userWalletId = params.userWallet.walletId, + blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), + cryptoCurrency = model.secondaryCurrency, + predefinedValues = PredefinedValues.Empty, + ), + onResult = model::onDestinationResult, + onClick = model::showEditDestination, + ) + + private val feeSelectorBlockComponent = feeSelectorBlockComponentFactory.create( + context = child("sendWithSwapConfirmFeeBlock"), + params = FeeSelectorParams.FeeSelectorBlockParams( + state = model.uiState.value.feeSelectorUM, + onLoadFee = model::loadFee, + feeCryptoCurrencyStatus = model.primaryFeePaidCurrencyStatus, + cryptoCurrencyStatus = model.primaryCurrencyStatus, + suggestedFeeState = FeeSelectorParams.SuggestedFeeState.None, + feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen, + ), + onResult = model::onFeeResult, + ) + + private val sendNotificationsComponent = sendNotificationsComponentFactory.create( + context = appComponentContext.childByContext(child("sendWithSwapConfirmNotifications")), + params = SendNotificationsComponent.Params( + analyticsCategoryName = params.analyticsCategoryName, + userWalletId = params.userWallet.walletId, + cryptoCurrencyStatus = model.primaryCurrencyStatus, + feeCryptoCurrencyStatus = model.primaryFeePaidCurrencyStatus, + appCurrency = params.appCurrency, + notificationData = SendNotificationsComponent.Params.NotificationData( + // todo fill with data + destinationAddress = "", + memo = "", + amountValue = BigDecimal.ZERO, + reduceAmountBy = BigDecimal.ZERO, + isIgnoreReduce = false, + fee = null, + feeError = null, + ), + ), + ) + + init { + model.uiState.onEach { state -> + val confirmUM = state.confirmUM as? ConfirmUM.Content + blockClickEnableFlow.value = confirmUM?.isTransactionInProcess == false + }.launchIn(componentScope) + } + + fun updateState(sendWithSwapUM: SendWithSwapUM) { + amountBlockComponent.updateState(sendWithSwapUM.amountUM) + sendDestinationBlockComponent.updateState(sendWithSwapUM.destinationUM) + feeSelectorBlockComponent.updateState(sendWithSwapUM.feeSelectorUM) + model.updateState(sendWithSwapUM) + } + + @Composable + override fun Content(modifier: Modifier) { + val sendWithSwapUM by model.uiState.collectAsStateWithLifecycle() + val sendNotificationsUM by sendNotificationsComponent.state.collectAsStateWithLifecycle() + + SendWithSwapConfirmContent( + sendWithSwapUM = sendWithSwapUM, + amountBlockComponent = amountBlockComponent, + sendDestinationBlockComponent = sendDestinationBlockComponent, + feeSelectorBlockComponent = feeSelectorBlockComponent, + sendNotificationsComponent = sendNotificationsComponent, + sendNotificationsUM = sendNotificationsUM, + modifier = modifier, + ) + } + + data class Params( + val sendWithSwapUM: SendWithSwapUM, + val analyticsCategoryName: String, + val userWallet: UserWallet, + val appCurrency: AppCurrency, + val currentRoute: Flow, + val swapDirection: SwapDirection, + val isBalanceHidingFlow: StateFlow, + val primaryCryptoCurrencyStatusFlow: StateFlow, + val primaryFeePaidCurrencyStatusFlow: StateFlow, + val callback: ModelCallback, + ) + + @AssistedFactory + interface Factory { + fun create(appComponentContext: AppComponentContext, params: Params): SendWithSwapConfirmComponent + } + + interface ModelCallback { + fun onResult(sendWithSwapUM: SendWithSwapUM) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/di/SendWithSwapConfirmModule.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/di/SendWithSwapConfirmModule.kt new file mode 100644 index 0000000000..30140080dc --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/di/SendWithSwapConfirmModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.swap.v2.impl.sendviaswap.confirm.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.SendWithSwapConfirmModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(ModelComponent::class) +internal interface SendWithSwapConfirmModule { + + @Binds + @IntoMap + @ClassKey(SendWithSwapConfirmModel::class) + fun bindsSendWithSwapConfirmModel(impl: SendWithSwapConfirmModel): Model +} \ No newline at end of file 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 new file mode 100644 index 0000000000..d89edaf05f --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -0,0 +1,239 @@ +package com.tangem.features.swap.v2.impl.sendviaswap.confirm.model + +import arrow.core.Either +import arrow.core.getOrElse +import arrow.core.left +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.navigationButtons.NavigationButton +import com.tangem.common.ui.navigationButtons.NavigationUM +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.express.models.ExpressProviderType +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase +import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection +import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.usecase.EstimateFeeUseCase +import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData +import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM +import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateTrigger +import com.tangem.features.swap.v2.impl.R +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.common.SwapUtils.INCREASE_GAS_LIMIT_FOR_CEX +import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute +import com.tangem.features.swap.v2.impl.sendviaswap.confirm.SendWithSwapConfirmComponent +import com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.transformers.SendWithSwapConfirmInitialStateTransformer +import com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.transformers.SendWithSwapConfirmationNotificationsTransformer +import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM +import com.tangem.lib.crypto.BlockchainFeeUtils.patchTransactionFeeForSwap +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.orZero +import jakarta.inject.Inject +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import com.tangem.utils.transformer.update as transformerUpdate + +@Suppress("LongParameterList") +@ModelScoped +internal class SendWithSwapConfirmModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val isSendTapHelpEnabledUseCase: IsSendTapHelpEnabledUseCase, + private val estimateFeeUseCase: EstimateFeeUseCase, + private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, + private val sendNotificationsUpdateTrigger: SendNotificationsUpdateTrigger, + paramsContainer: ParamsContainer, +) : Model(), FeeSelectorModelCallback { + + private val params: SendWithSwapConfirmComponent.Params = paramsContainer.require() + + val uiState: StateFlow + field = MutableStateFlow(params.sendWithSwapUM) + + val primaryCurrencyStatus: CryptoCurrencyStatus = params.primaryCryptoCurrencyStatusFlow.value + val primaryFeePaidCurrencyStatus: CryptoCurrencyStatus = params.primaryFeePaidCurrencyStatusFlow.value + + private val amountUM = uiState.value.amountUM as? SwapAmountUM.Content + + val secondaryCurrency: CryptoCurrency = requireNotNull(amountUM?.secondaryCryptoCurrencyStatus?.currency) { + "Crypto currency must not be null" + } + + private var isAmountSubtractAvailable = false + + init { + initAmountSubtractAvailability() + configConfirmNavigation() + initialState() + } + + override fun onFeeResult(feeSelectorUM: FeeSelectorUM) { + uiState.update { it.copy(feeSelectorUM = feeSelectorUM) } + updateConfirmNotifications() + } + + fun onAmountResult(amountUM: SwapAmountUM) { + uiState.update { it.copy(amountUM = amountUM) } + updateConfirmNotifications() + } + + fun onDestinationResult(destinationUM: DestinationUM) { + uiState.update { it.copy(destinationUM = destinationUM) } + updateConfirmNotifications() + } + + fun updateState(sendWithSwapUM: SendWithSwapUM) { + uiState.value = sendWithSwapUM + } + + fun showEditAmount() { + router.push(SendWithSwapRoute.Amount(isEditMode = true)) + } + + fun showEditDestination() { + router.push(SendWithSwapRoute.Destination(isEditMode = true)) + } + + suspend fun loadFee(): Either { + val defaultError = GetFeeError.UnknownError.left() + val quote = amountUM?.selectedQuote as? SwapQuoteUM.Content ?: return defaultError + val amountUM = uiState.value.amountUM as? SwapAmountUM.Content ?: return defaultError + + val amountField = amountUM.swapDirection.withSwapDirection( + onDirect = { amountUM.primaryAmount.amountField }, + onReverse = { amountUM.secondaryAmount.amountField }, + ) as? AmountState.Data ?: return defaultError + val amountValue = amountField.amountTextField.cryptoAmount.value ?: return defaultError + + return when (val providerType = quote.provider.type) { + ExpressProviderType.CEX -> { + estimateFeeUseCase( + amount = amountValue, + userWallet = params.userWallet, + cryptoCurrency = primaryCurrencyStatus.currency, + ).map { + it.patchTransactionFeeForSwap(INCREASE_GAS_LIMIT_FOR_CEX) + } + } + ExpressProviderType.DEX, + ExpressProviderType.DEX_BRIDGE, + -> { + // todo send with swap + GetFeeError.UnknownError.left() + } + ExpressProviderType.ONRAMP, + -> GetFeeError.DataError( + cause = IllegalStateException("Provider $providerType is not supported in Send With Swap"), + ).left() + } + } + + private fun onSendClick() { + // todo swap send tx + } + + private fun initAmountSubtractAvailability() { + modelScope.launch { + isAmountSubtractAvailable = + isAmountSubtractAvailableUseCase( + params.userWallet.walletId, + primaryCurrencyStatus.currency, + ).getOrElse { false } + } + } + + private fun initialState() { + val confirmUM = uiState.value.confirmUM + + modelScope.launch { + val isShowTapHelp = isSendTapHelpEnabledUseCase().getOrElse { false } + if (confirmUM is ConfirmUM.Empty) { + uiState.update { + it.copy( + confirmUM = SendWithSwapConfirmInitialStateTransformer( + isShowTapHelp = isShowTapHelp, + ).transform(uiState.value.confirmUM), + ) + } + updateConfirmNotifications() + } + } + } + + private fun updateConfirmNotifications() { + val amountUM = uiState.value.amountUM as? SwapAmountUM.Content ?: return + val destinationUM = uiState.value.destinationUM as? DestinationUM.Content ?: return + val feeSelectorUMContent = uiState.value.feeSelectorUM as? FeeSelectorUM.Content + val feeSelectorUMError = uiState.value.feeSelectorUM as? FeeSelectorUM.Error + + val amountField = amountUM.swapDirection.withSwapDirection( + onDirect = { amountUM.primaryAmount.amountField }, + onReverse = { amountUM.secondaryAmount.amountField }, + ) as? AmountState.Data ?: return + val enteredDestination = destinationUM.addressTextField.actualAddress + + modelScope.launch { + sendNotificationsUpdateTrigger.triggerUpdate( + data = NotificationData( + destinationAddress = enteredDestination, + memo = null, + amountValue = amountField.amountTextField.cryptoAmount.value.orZero(), + reduceAmountBy = amountField.reduceAmountBy.orZero(), + isIgnoreReduce = amountField.isIgnoreReduce, + fee = feeSelectorUMContent?.selectedFeeItem?.fee, + feeError = feeSelectorUMError?.error, + ), + ) + uiState.transformerUpdate( + SendWithSwapConfirmationNotificationsTransformer(), + ) + } + } + + private fun configConfirmNavigation() { + combine( + flow = uiState, + flow2 = params.currentRoute, + transform = { state, route -> state to route }, + ).filter { + it.second is SendWithSwapRoute.Confirm + }.onEach { (state, _) -> + val confirmUM = state.confirmUM + params.callback.onResult( + state.copy( + navigationUM = NavigationUM.Content( + title = resourceReference(id = R.string.send_with_swap_confirm_title), + subtitle = null, + backIconRes = R.drawable.ic_back_24, + backIconClick = router::pop, + primaryButton = NavigationButton( + textReference = resourceReference(R.string.common_send), + iconRes = R.drawable.ic_tangem_24, + isEnabled = confirmUM.isPrimaryButtonEnabled, + onClick = { + when (confirmUM) { + is ConfirmUM.Content -> if (confirmUM.isTransactionInProcess) { + return@NavigationButton + } else { + onSendClick() + } + else -> return@NavigationButton + } + }, + ), + ), + ), + ) + }.launchIn(modelScope) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmInitialStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmInitialStateTransformer.kt new file mode 100644 index 0000000000..4be5c0b56a --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmInitialStateTransformer.kt @@ -0,0 +1,20 @@ +package com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.transformers + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.persistentListOf + +internal class SendWithSwapConfirmInitialStateTransformer( + private val isShowTapHelp: Boolean, +) : Transformer { + override fun transform(prevState: ConfirmUM): ConfirmUM { + return ConfirmUM.Content( + isPrimaryButtonEnabled = false, + isTransactionInProcess = false, + showTapHelp = isShowTapHelp, + sendingFooter = TextReference.EMPTY, + notifications = persistentListOf(), + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt new file mode 100644 index 0000000000..014e57650d --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt @@ -0,0 +1,106 @@ +package com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.transformers + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkIfCustomFeeTooHigh +import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkIfCustomFeeTooLow +import com.tangem.features.swap.v2.impl.R +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM +import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toPersistentList + +internal class SendWithSwapConfirmationNotificationsTransformer : Transformer { + override fun transform(prevState: SendWithSwapUM): SendWithSwapUM { + val confirmUM = prevState.confirmUM as? ConfirmUM.Content ?: return prevState + val feeSelectorUM = prevState.feeSelectorUM as? FeeSelectorUM.Content ?: return prevState + + return prevState.copy( + confirmUM = confirmUM.copy( + sendingFooter = getSendingFooterText(feeSelectorUM, prevState.amountUM), + notifications = buildList { + addTooHighNotification(feeSelectorUM = feeSelectorUM) + addTooLowNotification(feeSelectorUM = feeSelectorUM) + }.toPersistentList(), + ), + ) + } + + private fun MutableList.addTooLowNotification(feeSelectorUM: FeeSelectorUM.Content) { + if (checkIfCustomFeeTooLow(feeSelectorUM = feeSelectorUM)) { + add(NotificationUM.Warning.FeeTooLow) + } + } + + private fun MutableList.addTooHighNotification(feeSelectorUM: FeeSelectorUM.Content) { + val (isFeeTooHigh, diff) = checkIfCustomFeeTooHigh(feeSelectorUM = feeSelectorUM) + if (isFeeTooHigh) { + add(NotificationUM.Warning.TooHigh(diff)) + } + } + + private fun getSendingFooterText(feeSelectorUM: FeeSelectorUM.Content, swapAmountUM: SwapAmountUM): TextReference { + val amountUM = swapAmountUM.swapDirection.withSwapDirection( + onDirect = { swapAmountUM.primaryAmount.amountField }, + onReverse = { swapAmountUM.secondaryAmount.amountField }, + ) as? AmountState.Data + val feeItem = feeSelectorUM.selectedFeeItem + val feeFiatRateUM = feeSelectorUM.feeFiatRateUM + + val appCurrency = feeFiatRateUM?.appCurrency + + if (amountUM == null || appCurrency == null) return TextReference.EMPTY + + val fiatAmountValue = amountUM.amountTextField.fiatAmount.value + val fiatFeeValue = feeItem.fee.amount.value?.multiply(feeFiatRateUM.rate) + + val fiatSendingValue = if (feeSelectorUM.feeExtraInfo.isFeeConvertibleToFiat) { + fiatFeeValue?.let { fiatAmountValue?.plus(it) } + } else { + fiatAmountValue + } + + val fiatSending = fiatSendingValue.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } + val fiatFee = TextReference.EMPTY + // todo send with swap footer + // formatFooterFiatFee( + // amount = feeItem.fee.amount.copy(value = fiatFeeValue), + // isFeeConvertibleToFiat = feeSelectorUM.feeExtraInfo.isFeeConvertibleToFiat, + // isFeeApproximate = feeSelectorUM.feeExtraInfo.isFeeApproximate, + // appCurrency = appCurrency, + // ) + + return if (feeSelectorUM.feeExtraInfo.isTronToken && feeItem.fee is Fee.Tron) { + // todo send with swap footer + // getTronTokenFeeSendingText( + // fee = feeItem.fee, + // fiatFee = fiatFee, + // fiatSending = stringReference(fiatSending), + // ) + TextReference.EMPTY + } else { + resourceReference( + id = if (feeSelectorUM.feeExtraInfo.isFeeConvertibleToFiat) { + R.string.send_summary_transaction_description + } else { + R.string.send_summary_transaction_description_no_fiat_fee + }, + formatArgs = wrappedList(fiatSending, fiatFee), + ) + } + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/ui/SendWithSwapConfirmContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/ui/SendWithSwapConfirmContent.kt new file mode 100644 index 0000000000..4386a93cb6 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/ui/SendWithSwapConfirmContent.kt @@ -0,0 +1,72 @@ +package com.tangem.features.swap.v2.impl.sendviaswap.confirm.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.SpacerHMax +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent +import com.tangem.features.send.v2.api.SendNotificationsComponent +import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent +import com.tangem.features.swap.v2.impl.amount.SwapAmountBlockComponent +import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM +import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM +import kotlinx.collections.immutable.ImmutableList + +@Suppress("LongParameterList") +@Composable +internal fun SendWithSwapConfirmContent( + sendWithSwapUM: SendWithSwapUM, + amountBlockComponent: SwapAmountBlockComponent, + sendDestinationBlockComponent: SendDestinationBlockComponent, + feeSelectorBlockComponent: FeeSelectorBlockComponent, + sendNotificationsComponent: SendNotificationsComponent, + sendNotificationsUM: ImmutableList, + modifier: Modifier = Modifier, +) { + val confirmUM = sendWithSwapUM.confirmUM as? ConfirmUM.Content + + Column(modifier = modifier) { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(horizontal = 12.dp), + ) { + item(key = "SendWithSwapAmountBlock") { + amountBlockComponent.Content(Modifier) + } + item(key = "SendWithSwapDestinationBlock") { + sendDestinationBlockComponent.Content(Modifier) + } + item(key = "SendWithSwapFeeBLock") { + Box( + modifier = Modifier.clip(RoundedCornerShape(16.dp)), + ) { + feeSelectorBlockComponent.Content(Modifier) + } + } + if (confirmUM != null) { + // tapHelp(isDisplay = confirmUM.showTapHelp) // todo + with(sendNotificationsComponent) { + content( + state = sendNotificationsUM, + isClickDisabled = confirmUM.isTransactionInProcess, + ) + } + // notifications( + // notifications = confirmUM.notifications, + // isClickDisabled = confirmUM.isTransactionInProcess, + // ) + } + } + SpacerHMax() + // todo + // SendingText(footerText = confirmUM?.sendingFooter ?: TextReference.EMPTY) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/entity/SendWithSwapUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/entity/SendWithSwapUM.kt new file mode 100644 index 0000000000..1a576345a6 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/entity/SendWithSwapUM.kt @@ -0,0 +1,15 @@ +package com.tangem.features.swap.v2.impl.sendviaswap.entity + +import com.tangem.common.ui.navigationButtons.NavigationUM +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM + +internal data class SendWithSwapUM( + val amountUM: SwapAmountUM, + val destinationUM: DestinationUM, + val feeSelectorUM: FeeSelectorUM, + val confirmUM: ConfirmUM, + val navigationUM: NavigationUM, +) \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainFeeUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainFeeUtils.kt new file mode 100644 index 0000000000..30f9d06686 --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainFeeUtils.kt @@ -0,0 +1,81 @@ +package com.tangem.lib.crypto + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import java.math.BigInteger +import java.math.RoundingMode + +/** + * !!!IMPORTANT!!! + * Methods for tuning transaction fee of different blockchains + * + * Temporary solution for domain specific logic for Blockchain. + * Instead of creating repositories and unnecessary and overkill use cases + */ +object BlockchainFeeUtils { + + private val HUNDRED_PERCENT = BigInteger("100") + + /** + * We need to increase gasLimit for Ethereum fees for 2 cases + * + * DEX: for dex calculated gasLimit for given data might be changed when transaction processing + * for that case dex providers recommend to increase gasLimit for few percents to ensure transaction completes + * + * CEX: for that case we calculate fee for random generated address and gasLimit might be different for it + * and result address to send. That's why we should increase gasLimit a little + * + */ + fun TransactionFee.patchTransactionFeeForSwap(increaseBy: Int): TransactionFee { + return when (this) { + is TransactionFee.Choosable -> { + this.copy( + minimum = this.minimum.increaseEthGasLimitInNeeded(increaseBy), + normal = this.normal.increaseEthGasLimitInNeeded(increaseBy), + priority = this.priority.increaseEthGasLimitInNeeded(increaseBy), + ) + } + is TransactionFee.Single -> this.copy(normal = this.normal.increaseEthGasLimitInNeeded(increaseBy)) + } + } + + private fun Fee.increaseEthGasLimitInNeeded(increaseBy: Int): Fee { + return when (this) { + is Fee.Ethereum.EIP1559, + is Fee.Ethereum.Legacy, + -> this.increaseGasLimitBy(increaseBy) + is Fee.Alephium, + is Fee.Aptos, + is Fee.Bitcoin, + is Fee.CardanoToken, + is Fee.Common, + is Fee.Filecoin, + is Fee.Hedera, + is Fee.Kaspa, + is Fee.Sui, + is Fee.Tron, + is Fee.VeChain, + -> this + } + } + + /** + * Increase gasLimit for Fee.Ethereum + */ + private fun Fee.increaseGasLimitBy(percentage: Int): Fee { + if (this !is Fee.Ethereum) return this + val gasLimit = this.gasLimit + val increasedGasPrice = this.amount.value?.movePointRight(this.amount.decimals) + ?.divide(gasLimit.toBigDecimal(), RoundingMode.HALF_UP) + val increasedGasLimit = gasLimit + .multiply(percentage.toBigInteger()) + .divide(HUNDRED_PERCENT) + val increasedAmount = this.amount.copy( + value = increasedGasLimit.toBigDecimal().multiply(increasedGasPrice).movePointLeft(this.amount.decimals), + ) + return when (this) { + is Fee.Ethereum.EIP1559 -> copy(amount = increasedAmount, gasLimit = increasedGasLimit) + is Fee.Ethereum.Legacy -> copy(amount = increasedAmount, gasLimit = increasedGasLimit) + } + } +} \ No newline at end of file