Updated on 2026-08-14
This commit is contained in:
parent
5988ef1834
commit
bdaa181aec
13 changed files with 790 additions and 0 deletions
|
|
@ -13,4 +13,12 @@ import com.tangem.domain.swap.models.SwapDirection.Reverse
|
||||||
enum class SwapDirection {
|
enum class SwapDirection {
|
||||||
Direct,
|
Direct,
|
||||||
Reverse,
|
Reverse,
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
inline fun <T> SwapDirection.withSwapDirection(onDirect: () -> T, onReverse: () -> T): T = when (this) {
|
||||||
|
Direct -> onDirect()
|
||||||
|
Reverse -> onReverse()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -34,6 +34,9 @@ dependencies {
|
||||||
implementation(projects.common.ui)
|
implementation(projects.common.ui)
|
||||||
implementation(projects.common.routing)
|
implementation(projects.common.routing)
|
||||||
|
|
||||||
|
/** Libs */
|
||||||
|
implementation(projects.libs.crypto)
|
||||||
|
|
||||||
implementation(tangemDeps.blockchain) {
|
implementation(tangemDeps.blockchain) {
|
||||||
exclude(module = "joda-time")
|
exclude(module = "joda-time")
|
||||||
}
|
}
|
||||||
|
|
@ -54,6 +57,9 @@ dependencies {
|
||||||
implementation(projects.domain.transaction.models)
|
implementation(projects.domain.transaction.models)
|
||||||
implementation(projects.domain.transaction)
|
implementation(projects.domain.transaction)
|
||||||
implementation(projects.domain.legacy)
|
implementation(projects.domain.legacy)
|
||||||
|
implementation(projects.domain.balanceHiding.models)
|
||||||
|
implementation(projects.domain.balanceHiding)
|
||||||
|
implementation(projects.domain.settings)
|
||||||
|
|
||||||
/** Compose */
|
/** Compose */
|
||||||
implementation(deps.compose.foundation)
|
implementation(deps.compose.foundation)
|
||||||
|
|
|
||||||
|
|
@ -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%
|
||||||
|
}
|
||||||
|
|
@ -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<NotificationUM>,
|
||||||
|
) : ConfirmUM()
|
||||||
|
|
||||||
|
data object Empty : ConfirmUM() {
|
||||||
|
override val isPrimaryButtonEnabled: Boolean = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<SendWithSwapRoute>,
|
||||||
|
val swapDirection: SwapDirection,
|
||||||
|
val isBalanceHidingFlow: StateFlow<Boolean>,
|
||||||
|
val primaryCryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>,
|
||||||
|
val primaryFeePaidCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>,
|
||||||
|
val callback: ModelCallback,
|
||||||
|
)
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
fun create(appComponentContext: AppComponentContext, params: Params): SendWithSwapConfirmComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModelCallback {
|
||||||
|
fun onResult(sendWithSwapUM: SendWithSwapUM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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<SendWithSwapUM>
|
||||||
|
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<GetFeeError, TransactionFee> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<ConfirmUM> {
|
||||||
|
override fun transform(prevState: ConfirmUM): ConfirmUM {
|
||||||
|
return ConfirmUM.Content(
|
||||||
|
isPrimaryButtonEnabled = false,
|
||||||
|
isTransactionInProcess = false,
|
||||||
|
showTapHelp = isShowTapHelp,
|
||||||
|
sendingFooter = TextReference.EMPTY,
|
||||||
|
notifications = persistentListOf(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<SendWithSwapUM> {
|
||||||
|
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<NotificationUM>.addTooLowNotification(feeSelectorUM: FeeSelectorUM.Content) {
|
||||||
|
if (checkIfCustomFeeTooLow(feeSelectorUM = feeSelectorUM)) {
|
||||||
|
add(NotificationUM.Warning.FeeTooLow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MutableList<NotificationUM>.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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<NotificationUM>,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue