diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 078e47c07e..5169a4b83a 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -436,4 +436,10 @@ internal object TokensDomainModule { fun provideCheckHasLinkedTokensUseCase(currenciesRepository: CurrenciesRepository): CheckHasLinkedTokensUseCase { return CheckHasLinkedTokensUseCase(currenciesRepository) } + + @Provides + @Singleton + fun provideGetCurrencyCheckUseCase(currencyChecksRepository: CurrencyChecksRepository): GetCurrencyCheckUseCase { + return GetCurrencyCheckUseCase(currencyChecksRepository) + } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/feeScreen/FeeCalculation.kt b/common/ui/src/main/java/com/tangem/common/ui/feeScreen/FeeCalculation.kt new file mode 100644 index 0000000000..d1c3719c0a --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/feeScreen/FeeCalculation.kt @@ -0,0 +1,86 @@ +package com.tangem.common.ui.feeScreen + +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.extensions.isZero +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import java.math.BigDecimal +import java.math.RoundingMode + +/** + * Check and calculates subtracted amount + */ +fun checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable: Boolean, + cryptoCurrencyStatus: CryptoCurrencyStatus, + amountValue: BigDecimal, + feeValue: BigDecimal, + reduceAmountBy: BigDecimal, +): BigDecimal { + val balance = cryptoCurrencyStatus.value.amount ?: return amountValue + val isFeeCoverage = checkFeeCoverage( + isSubtractAvailable = isAmountSubtractAvailable, + balance = balance, + amountValue = amountValue, + feeValue = feeValue, + reduceAmountBy = reduceAmountBy, + ) + return if (isFeeCoverage) { + balance.minus(reduceAmountBy).minus(feeValue) + } else { + amountValue + } +} + +/** + * Checks if sending amount with fee is greater than balance + */ +fun checkFeeCoverage( + isSubtractAvailable: Boolean, + balance: BigDecimal, + amountValue: BigDecimal, + feeValue: BigDecimal, + reduceAmountBy: BigDecimal?, +): Boolean { + if (!isSubtractAvailable) return false + val reducedBy = balance - (reduceAmountBy ?: BigDecimal.ZERO) + return reducedBy < amountValue + feeValue && reducedBy > feeValue && reducedBy >= amountValue +} + +/** + * Check if custom fee is too low + */ +fun checkIfFeeTooLow(fee: TransactionFee, customValue: BigDecimal, isCustomSelected: Boolean): Boolean { + val multipleFees = fee as? TransactionFee.Choosable ?: return false + val minimumValue = multipleFees.minimum.amount.value ?: return false + + return isCustomSelected && minimumValue > customValue +} + +/** + * Check if custom fee is too high + */ +fun checkIfFeeTooHigh( + fee: TransactionFee, + customValue: BigDecimal, + isCustomSelected: Boolean, + onShow: (String) -> Unit, +): Boolean { + val multipleFees = fee as? TransactionFee.Choosable ?: return false + val highValue = multipleFees.priority.amount.value ?: return false + + val diff = customValue / highValue + val isShow = isCustomSelected && diff > FEE_MAX_DIFF + if (isShow) onShow(diff.parseBigDecimal(ZERO_DECIMALS, RoundingMode.HALF_UP)) + return isShow +} + +/** + * Checks if fee exceeds fee paid currency balance + */ +fun checkExceedBalance(feeBalance: BigDecimal?, feeAmount: BigDecimal?): Boolean { + return feeAmount == null || feeBalance == null || feeAmount.isZero() || feeAmount > feeBalance +} + +private val FEE_MAX_DIFF = BigDecimal("5") +private const val ZERO_DECIMALS = 0 \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyCheck.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyCheck.kt new file mode 100644 index 0000000000..04018ed4aa --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyCheck.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.tokens.model.warnings + +import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit +import java.math.BigDecimal + +data class CryptoCurrencyCheck( + val dustValue: BigDecimal?, + val reserveAmount: BigDecimal?, + val existentialDeposit: BigDecimal?, + val utxoAmountLimit: UtxoAmountLimit?, +) \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt new file mode 100644 index 0000000000..4e74a21591 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt @@ -0,0 +1,41 @@ +package com.tangem.domain.tokens + +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck +import com.tangem.domain.tokens.repository.CurrencyChecksRepository +import com.tangem.domain.wallets.models.UserWalletId +import java.math.BigDecimal + +class GetCurrencyCheckUseCase( + private val currencyChecksRepository: CurrencyChecksRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + currencyStatus: CryptoCurrencyStatus, + amount: BigDecimal?, + fee: BigDecimal?, + ): CryptoCurrencyCheck { + val network = currencyStatus.currency.network + val dustValue = currencyChecksRepository.getDustValue(userWalletId, network) + val reserveAmount = currencyChecksRepository.getReserveAmount(userWalletId, network) + val existentialDeposit = currencyChecksRepository.getExistentialDeposit(userWalletId, network) + val utxoAmountLimit = if (amount != null && fee != null) { + currencyChecksRepository.checkUtxoAmountLimit( + userWalletId = userWalletId, + network = network, + amount = amount, + fee = fee, + ) + } else { + null + } + + return CryptoCurrencyCheck( + dustValue = dustValue, + reserveAmount = reserveAmount, + existentialDeposit = existentialDeposit, + utxoAmountLimit = utxoAmountLimit, + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt index c64b0dd4f2..1e01cf0b3d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt @@ -1,8 +1,9 @@ package com.tangem.features.staking.impl.di +import com.tangem.common.routing.AppRouter import com.tangem.core.navigation.url.UrlOpener -import com.tangem.features.staking.impl.navigation.DefaultStakingRouter import com.tangem.features.staking.api.navigation.StakingRouter +import com.tangem.features.staking.impl.navigation.DefaultStakingRouter import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -18,9 +19,10 @@ internal object StakingRouterModule { @Provides @ActivityScoped - fun provideStakingRouter(urlOpener: UrlOpener): StakingRouter { + fun provideStakingRouter(urlOpener: UrlOpener, router: AppRouter): StakingRouter { return DefaultStakingRouter( urlOpener = urlOpener, + router = router, ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt index 464d28694e..6acad0658b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt @@ -1,15 +1,33 @@ package com.tangem.features.staking.impl.navigation import androidx.fragment.app.Fragment +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.staking.impl.presentation.StakingFragment internal class DefaultStakingRouter( private val urlOpener: UrlOpener, + private val router: AppRouter, ) : InnerStakingRouter { override fun getEntryFragment(): Fragment = StakingFragment.create() override fun openUrl(url: String) { urlOpener.openUrl(url) } + + override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) { + router.pop { isSuccess -> + if (isSuccess) { + router.push( + AppRoute.CurrencyDetails( + userWalletId = userWalletId, + currency = currency, + ), + ) + } + } + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt index d4197489fb..e1fec4bab1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt @@ -1,8 +1,12 @@ package com.tangem.features.staking.impl.navigation +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.staking.api.navigation.StakingRouter interface InnerStakingRouter : StakingRouter { fun openUrl(url: String) + + fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt new file mode 100644 index 0000000000..2776bf6869 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt @@ -0,0 +1,186 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.feeScreen.checkAndCalculateSubtractedAmount +import com.tangem.common.ui.feeScreen.checkFeeCoverage +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.common.ui.notifications.NotificationsFactory.addDustWarningNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addExceedBalanceNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addExceedsBalanceNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addExistentialWarningNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addFeeCoverageNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addFeeUnreachableNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountErrorNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addValidateTransactionNotifications +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.features.staking.impl.presentation.state.FeeState +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.utils.Provider +import com.tangem.utils.extensions.orZero +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal + +@Suppress("LongParameterList") +internal class AddStakingNotificationsTransformer( + private val cryptoCurrencyStatusProvider: Provider, + private val appCurrencyProvider: Provider, + private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?, + private val currencyWarning: CryptoCurrencyWarning?, + private val validatorError: Throwable?, + private val feeError: GetFeeError?, + private val currencyCheck: CryptoCurrencyCheck, + private val isSubtractAvailable: Boolean, +) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val appCurrency = appCurrencyProvider() + val balance = cryptoCurrencyStatus.value.amount.orZero() + + val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState + val amountState = prevState.amountState as? AmountState.Data ?: return prevState + val feeState = confirmationState.feeState as? FeeState.Content ?: return prevState + + val amountValue = amountState.amountTextField.cryptoAmount.value.orZero() + val feeValue = feeState.fee?.amount?.value.orZero() + val reduceAmountBy = confirmationState.reduceAmountBy.orZero() + + val isFeeCoverage = checkFeeCoverage( + amountValue = amountValue, + feeValue = feeValue, + balance = balance, + isSubtractAvailable = isSubtractAvailable, + reduceAmountBy = reduceAmountBy, + ) + val sendingAmount = checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable = isSubtractAvailable, + cryptoCurrencyStatus = cryptoCurrencyStatusProvider(), + amountValue = amountValue, + feeValue = feeValue, + reduceAmountBy = reduceAmountBy, + ) + + val notifications = buildList { + // errors + addErrorNotifications( + prevState = prevState, + feeError = feeError, + sendingAmount = sendingAmount, + onReload = { prevState.clickIntents.loadFee(confirmationState.pendingActions) }, + feeValue = feeValue, + ) + // warnings + addWarningNotifications( + prevState = prevState, + amountState = amountState, + feeState = feeState, + sendingAmount = sendingAmount, + isFeeCoverage = isFeeCoverage, + ) + + addAll(confirmationState.notifications) + }.toImmutableList() + + return prevState.copy( + confirmationState = confirmationState.copy( + notifications = notifications.toImmutableList(), + ), + ) + } + + private fun MutableList.addErrorNotifications( + prevState: StakingUiState, + onReload: () -> Unit, + feeError: GetFeeError?, + sendingAmount: BigDecimal, + feeValue: BigDecimal, + ) { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val cryptoCurrency = cryptoCurrencyStatus.currency + val network = cryptoCurrency.network + + if (feeError != null) { + addFeeUnreachableNotification( + feeError = feeError, + tokenName = cryptoCurrencyStatusProvider().currency.name, + onReload = onReload, + ) + } + addExceedBalanceNotification( + feeAmount = feeValue, + sendingAmount = sendingAmount, + isSubtractionAvailable = isSubtractAvailable, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ) + addExceedsBalanceNotification( + cryptoCurrencyWarning = currencyWarning, + cryptoCurrencyStatus = cryptoCurrencyStatus, + shouldMergeFeeNetworkName = BlockchainUtils.isArbitrum(network.backendId), + onClick = prevState.clickIntents::openTokenDetails, + onAnalyticsEvent = { /* [REDACTED_TODO_COMMENT] */ }, + ) + if (!BlockchainUtils.isCardano(network.id.value)) { + addDustWarningNotification( + dustValue = currencyCheck.dustValue, + feeValue = feeValue, + sendingAmount = sendingAmount, + cryptoCurrencyStatus = cryptoCurrencyStatus, + feeCurrencyStatus = feeCryptoCurrencyStatus, + ) + } + addTransactionLimitErrorNotification( + utxoLimit = currencyCheck.utxoAmountLimit, + cryptoCurrency = cryptoCurrency, + onReduceClick = prevState.clickIntents::onAmountReduceToClick, + ) + addReserveAmountErrorNotification( + reserveAmount = currencyCheck.reserveAmount, + sendingAmount = sendingAmount, + cryptoCurrency = cryptoCurrency, + isAccountFunded = false, + ) + } + + private fun MutableList.addWarningNotifications( + prevState: StakingUiState, + amountState: AmountState.Data, + feeState: FeeState.Content, + sendingAmount: BigDecimal, + isFeeCoverage: Boolean, + ) { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val appCurrency = appCurrencyProvider() + val cryptoCurrency = cryptoCurrencyStatus.currency + + addExistentialWarningNotification( + existentialDeposit = currencyCheck.existentialDeposit, + feeAmount = feeState.fee?.amount?.value.orZero(), + receivedAmount = sendingAmount, + cryptoCurrencyStatus = cryptoCurrencyStatus, + onReduceClick = prevState.clickIntents::onAmountReduceByClick, + ) + addFeeCoverageNotification( + isFeeCoverage = isFeeCoverage, + amountField = amountState.amountTextField, + sendingValue = sendingAmount, + appCurrency = appCurrency, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ) + + // blockchain specific + addValidateTransactionNotifications( + dustValue = currencyCheck.dustValue.orZero(), + fee = feeState.fee, + validationError = validatorError, + cryptoCurrency = cryptoCurrency, + onReduceClick = prevState.clickIntents::onAmountReduceToClick, + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt new file mode 100644 index 0000000000..91468a21bd --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt @@ -0,0 +1,19 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.amount + +import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer +import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer.ReduceByData +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer + +internal class AmountReduceByStateTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val value: ReduceByData, +) : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + amountState = AmountReduceByTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt new file mode 100644 index 0000000000..b826aae59e --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt @@ -0,0 +1,18 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.amount + +import com.tangem.common.ui.amountScreen.converters.AmountReduceToTransformer +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer +import java.math.BigDecimal + +internal class AmountReduceToStateTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val value: BigDecimal, +) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + amountState = AmountReduceToTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState), + ) + } +} \ No newline at end of file