diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 544358e464..adbdabe422 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 544358e4640ae25402dd4186e0f244f9509c8906 +Subproject commit adbdabe422b0513640d1750f276298683d342f61 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 9f805a3571..9bea3ac624 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 @@ -268,6 +268,16 @@ internal object TokensDomainModule { ) } + @Provides + @Singleton + fun provideGetMinimumTransactionAmountSyncUseCase( + currencyChecksRepository: CurrencyChecksRepository, + ): GetMinimumTransactionAmountSyncUseCase { + return GetMinimumTransactionAmountSyncUseCase( + currencyChecksRepository = currencyChecksRepository, + ) + } + @Provides @Singleton fun provideIsCryptoCurrencyCoinCouldHideUseCase( diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt index 8f80642201..9e980da2ba 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt @@ -207,6 +207,10 @@ internal class DefaultLegacyWalletConnectRepository( Timber.i("onSessionDelete: $sessionDelete") } + override fun onSessionExtend(session: Wallet.Model.Session) { + Timber.i("onSessionExtend: $session") + } + override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) { // Triggered when wallet receives the session settlement response from Dapp Timber.i("onSessionSettleResponse: $settleSessionResponse") @@ -264,8 +268,6 @@ internal class DefaultLegacyWalletConnectRepository( } override fun approve(userNamespaces: Map>) { - this.userNamespaces = userNamespaces - val sessionProposal: Wallet.Model.SessionProposal = requireNotNull(this.sessionProposal) val userChains = userNamespaces.flatMap { namespace -> diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index 91726c0820..d4f05a284a 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -6,7 +6,6 @@ import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.models.Basic import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.domain.appcurrency.FetchAppCurrenciesUseCase import com.tangem.domain.balancehiding.BalanceHidingSettings @@ -99,7 +98,6 @@ internal class MainViewModel @Inject constructor( .distinctUntilChanged() .onEach { userWallet -> Analytics.setContext(userWallet.scanResponse) - Analytics.send(Basic.WalletOpened()) } .flowOn(dispatchers.io) .launchIn(viewModelScope) diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt index 0b9f4c1a5d..4fbf0d991a 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt @@ -4,10 +4,15 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.KeyboardType import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.common.ui.amountScreen.utils.checkExceedBalance import com.tangem.common.ui.amountScreen.utils.getFiatValue import com.tangem.common.ui.amountScreen.utils.getKeyboardAction +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.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.utils.isNullOrZero @@ -22,6 +27,7 @@ import java.math.BigDecimal */ class AmountReduceByTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val minimumTransactionAmount: EnterAmountBoundary?, private val value: ReduceByData, ) : Transformer { @@ -47,22 +53,37 @@ class AmountReduceByTransformer( val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue val isExceedBalance = checkValue.checkExceedBalance(maxEnterAmount, amountTextField) + val isLessThanMinimumIfProvided = minimumTransactionAmount?.amount?.let { decimalCryptoValue < it } ?: false val isZero = if (amountTextField.isFiatValue) { decimalFiatValue.isNullOrZero() } else { decimalCryptoValue.isNullOrZero() } + val isCheckFailed = isExceedBalance || isLessThanMinimumIfProvided return prevState.copy( - isPrimaryButtonEnabled = !isExceedBalance && !isZero, + isPrimaryButtonEnabled = !isZero && !isCheckFailed, amountTextField = amountTextField.copy( value = cryptoValue, fiatValue = fiatValue, - isError = isExceedBalance, - error = resourceReference(R.string.send_validation_amount_exceeds_balance), + isError = isCheckFailed, + error = when { + isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance) + isLessThanMinimumIfProvided -> { + val minimumAmount = minimumTransactionAmount + ?.amount + ?.format { crypto(cryptoCurrencyStatus.currency) } + .orEmpty() + resourceReference( + R.string.transfer_notification_invalid_minimum_transaction_amount_text, + wrappedList(minimumAmount, minimumAmount), + ) + } + else -> TextReference.EMPTY + }, cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), keyboardOptions = KeyboardOptions( - imeAction = getKeyboardAction(isExceedBalance, decimalCryptoValue), + imeAction = getKeyboardAction(isCheckFailed, decimalCryptoValue), keyboardType = KeyboardType.Number, ), ), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt index 74f6e66852..7e5cf0ab8d 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt @@ -4,11 +4,17 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.KeyboardType import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.common.ui.amountScreen.utils.checkExceedBalance import com.tangem.common.ui.amountScreen.utils.getFiatValue import com.tangem.common.ui.amountScreen.utils.getKeyboardAction +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.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.Transformer @@ -22,6 +28,7 @@ import java.math.BigDecimal */ class AmountReduceToTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val minimumTransactionAmount: EnterAmountBoundary?, private val value: BigDecimal, ) : Transformer { private val maxEnterAmountConverter = MaxEnterAmountConverter() @@ -34,6 +41,7 @@ class AmountReduceToTransformer( val fiatDecimals = amountTextField.fiatAmount.decimals val cryptoValue = value.parseBigDecimal(cryptoDecimals) + val decimalCryptoValue = cryptoValue.parseToBigDecimal(cryptoDecimals) val (fiatValue, decimalFiatValue) = cryptoValue.getFiatValue( fiatRate = cryptoCurrencyStatus.value.fiatRate, isFiatValue = false, @@ -44,18 +52,33 @@ class AmountReduceToTransformer( val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue val isExceedBalance = checkValue.checkExceedBalance(maxEnterAmount, amountTextField) + val isLessThanMinimumIfProvided = minimumTransactionAmount?.amount?.let { decimalCryptoValue < it } ?: false val isZero = if (amountTextField.isFiatValue) decimalFiatValue.isNullOrZero() else value.isNullOrZero() + val isCheckFailed = isExceedBalance || isLessThanMinimumIfProvided return prevState.copy( - isPrimaryButtonEnabled = !isExceedBalance && !isZero, + isPrimaryButtonEnabled = !isZero && !isCheckFailed, amountTextField = amountTextField.copy( value = cryptoValue, fiatValue = fiatValue, - isError = isExceedBalance, - error = resourceReference(R.string.send_validation_amount_exceeds_balance), + isError = isCheckFailed, + error = when { + isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance) + isLessThanMinimumIfProvided -> { + val minimumAmount = minimumTransactionAmount + ?.amount + ?.format { crypto(cryptoCurrencyStatus.currency) } + .orEmpty() + resourceReference( + R.string.transfer_notification_invalid_minimum_transaction_amount_text, + wrappedList(minimumAmount, minimumAmount), + ) + } + else -> TextReference.EMPTY + }, cryptoAmount = amountTextField.cryptoAmount.copy(value = value), fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), keyboardOptions = KeyboardOptions( - imeAction = getKeyboardAction(isExceedBalance, value), + imeAction = getKeyboardAction(isCheckFailed, decimalCryptoValue), keyboardType = KeyboardType.Number, ), ), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt index e2adb46d11..f91822b7b7 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt @@ -6,14 +6,14 @@ import com.tangem.common.ui.amountScreen.converters.field.AmountFieldConverter import com.tangem.common.ui.amountScreen.models.AmountParameters import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig import com.tangem.common.ui.amountScreen.models.AmountState -import com.tangem.common.ui.amountScreen.models.MaxEnterAmount +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.utils.Provider @@ -26,7 +26,7 @@ import kotlinx.collections.immutable.persistentListOf * * @property clickIntents amount screen clicks * @property appCurrencyProvider selected app currency provider - * @property maxEnterAmountProvider max enter amount data provider + * @property maxEnterAmount max enter amount data * @property cryptoCurrencyStatusProvider current cryptocurrency status provider * @property iconStateConverter currency icon converter */ @@ -34,7 +34,7 @@ class AmountStateConverter( private val clickIntents: AmountScreenClickIntents, private val appCurrencyProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, - private val maxEnterAmountProvider: Provider, + private val maxEnterAmount: EnterAmountBoundary, private val iconStateConverter: CryptoCurrencyToIconStateConverter, ) : Converter { @@ -47,10 +47,9 @@ class AmountStateConverter( } override fun convert(value: AmountParameters): AmountState { - val maxEnterAmount = maxEnterAmountProvider() val appCurrency = appCurrencyProvider() val status = cryptoCurrencyStatusProvider() - val fiat = formatFiatAmount(maxEnterAmount.fiatAmount, appCurrency.code, appCurrency.symbol) + val fiat = maxEnterAmount.fiatAmount.format { fiat(appCurrency.code, appCurrency.symbol) } val crypto = maxEnterAmount.amount.format { crypto(status.currency) } val noFeeRate = status.value.fiatRate.isNullOrZero() diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/MaxEnterAmountConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/MaxEnterAmountConverter.kt index 1b23b64181..dce1957361 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/MaxEnterAmountConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/MaxEnterAmountConverter.kt @@ -1,16 +1,16 @@ package com.tangem.common.ui.amountScreen.converters -import com.tangem.common.ui.amountScreen.models.MaxEnterAmount +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.utils.converter.Converter /** - * Converts [CryptoCurrencyStatus] to [MaxEnterAmount] + * Converts [CryptoCurrencyStatus] to [EnterAmountBoundary] */ -class MaxEnterAmountConverter : Converter { +class MaxEnterAmountConverter : Converter { - override fun convert(value: CryptoCurrencyStatus): MaxEnterAmount { - return MaxEnterAmount( + override fun convert(value: CryptoCurrencyStatus): EnterAmountBoundary { + return EnterAmountBoundary( amount = value.value.amount, fiatAmount = value.value.fiatAmount, fiatRate = value.value.fiatRate, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt index b51fa49ad2..392fa817ba 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt @@ -5,14 +5,18 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountState -import com.tangem.common.ui.amountScreen.models.MaxEnterAmount +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.common.ui.amountScreen.utils.checkExceedBalance import com.tangem.common.ui.amountScreen.utils.getCryptoValue import com.tangem.common.ui.amountScreen.utils.getFiatValue import com.tangem.common.ui.amountScreen.utils.getKeyboardAction 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.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.Transformer import java.math.BigDecimal @@ -24,7 +28,9 @@ import java.math.BigDecimal * @property value amount value */ class AmountFieldChangeTransformer( - private val maxEnterAmount: MaxEnterAmount, + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val maxEnterAmount: EnterAmountBoundary, + private val minimumTransactionAmount: EnterAmountBoundary?, private val value: String, ) : Transformer { @@ -52,23 +58,37 @@ class AmountFieldChangeTransformer( val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue val isExceedBalance = checkValue.checkExceedBalance(maxEnterAmount, amountTextField) + val isLessThanMinimumIfProvided = minimumTransactionAmount?.amount?.let { decimalCryptoValue < it } ?: false val isZero = if (amountTextField.isFiatValue) { decimalFiatValue.isNullOrZero() } else { decimalCryptoValue.isNullOrZero() } + val isCheckFailed = isExceedBalance || isLessThanMinimumIfProvided return prevState.copy( - isPrimaryButtonEnabled = !isExceedBalance && !isZero, + isPrimaryButtonEnabled = !isZero && !isCheckFailed, amountTextField = amountTextField.copy( value = cryptoValue, fiatValue = fiatValue, - isError = isExceedBalance, - error = resourceReference(R.string.send_validation_amount_exceeds_balance).takeIf { isExceedBalance } - ?: TextReference.EMPTY, + isError = isCheckFailed, + error = when { + isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance) + isLessThanMinimumIfProvided -> { + val minimumAmount = minimumTransactionAmount + ?.amount + ?.format { crypto(cryptoCurrencyStatus.currency) } + .orEmpty() + resourceReference( + R.string.transfer_notification_invalid_minimum_transaction_amount_text, + wrappedList(minimumAmount, minimumAmount), + ) + } + else -> TextReference.EMPTY + }, cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), keyboardOptions = KeyboardOptions( - imeAction = getKeyboardAction(isExceedBalance, decimalCryptoValue), + imeAction = getKeyboardAction(isCheckFailed, decimalCryptoValue), keyboardType = KeyboardType.Number, ), ), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt index 16c67d20f8..990e027cdf 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt @@ -1,12 +1,19 @@ package com.tangem.common.ui.amountScreen.converters.field import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType +import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountState -import com.tangem.common.ui.amountScreen.models.MaxEnterAmount +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary +import com.tangem.common.ui.amountScreen.utils.getKeyboardAction +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.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseBigDecimal -import com.tangem.utils.isNullOrZero +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.utils.extensions.isZero import com.tangem.utils.transformer.Transformer import java.math.RoundingMode @@ -16,7 +23,9 @@ import java.math.RoundingMode * @property maxAmount maximum enter amount */ class AmountFieldSetMaxAmountTransformer( - private val maxAmount: MaxEnterAmount, + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val maxAmount: EnterAmountBoundary, + private val minAmount: EnterAmountBoundary?, ) : Transformer { override fun transform(prevState: AmountState): AmountState { @@ -29,22 +38,35 @@ class AmountFieldSetMaxAmountTransformer( val decimalCryptoValue = maxAmount.amount val decimalFiatValue = maxAmount.fiatAmount - if (decimalCryptoValue.isNullOrZero()) return prevState + if (decimalCryptoValue == null || decimalCryptoValue.isZero()) return prevState - val isDoneActionEnabled = !decimalCryptoValue.isNullOrZero() - val cryptoValue = decimalCryptoValue?.parseBigDecimal(cryptoDecimals).orEmpty() + val cryptoValue = decimalCryptoValue.parseBigDecimal(cryptoDecimals) val fiatValue = decimalFiatValue?.parseBigDecimal(fiatDecimals, roundingMode = RoundingMode.HALF_UP).orEmpty() + val isLessThanMinimumIfProvided = minAmount?.amount?.let { decimalCryptoValue < it } ?: false return prevState.copy( - isPrimaryButtonEnabled = true, + isPrimaryButtonEnabled = !isLessThanMinimumIfProvided, amountTextField = amountTextField.copy( isValuePasted = true, value = cryptoValue, fiatValue = fiatValue, - isError = false, + isError = isLessThanMinimumIfProvided, + error = when { + isLessThanMinimumIfProvided -> { + val minimumAmount = minAmount + ?.amount + ?.format { crypto(cryptoCurrencyStatus.currency) } + .orEmpty() + resourceReference( + R.string.transfer_notification_invalid_minimum_transaction_amount_text, + wrappedList(minimumAmount, minimumAmount), + ) + } + else -> TextReference.EMPTY + }, cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), keyboardOptions = KeyboardOptions( - imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None, + imeAction = getKeyboardAction(isLessThanMinimumIfProvided, decimalCryptoValue), keyboardType = KeyboardType.Number, ), ), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/EnterAmountBoundary.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/EnterAmountBoundary.kt new file mode 100644 index 0000000000..bc21ab5104 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/EnterAmountBoundary.kt @@ -0,0 +1,22 @@ +package com.tangem.common.ui.amountScreen.models + +import java.math.BigDecimal + +data class EnterAmountBoundary( + val amount: BigDecimal? = null, + val fiatAmount: BigDecimal? = null, + val fiatRate: BigDecimal? = null, +) { + constructor( + amount: BigDecimal? = null, + fiatRate: BigDecimal? = null, + ) : this( + amount = amount, + fiatAmount = if (amount != null && fiatRate != null) { + amount * fiatRate + } else { + null + }, + fiatRate = fiatRate, + ) +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/MaxEnterAmount.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/MaxEnterAmount.kt deleted file mode 100644 index e9a5f88238..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/MaxEnterAmount.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.common.ui.amountScreen.models - -import java.math.BigDecimal - -data class MaxEnterAmount( - val amount: BigDecimal? = null, - val fiatAmount: BigDecimal? = null, - val fiatRate: BigDecimal? = null, -) \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt index c74f183aa6..f2f8e078f9 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt @@ -138,7 +138,7 @@ private fun AmountFieldError( exit = fadeOut(), modifier = modifier, ) { - val errorText = remember(this) { error } + val errorText = remember(this, error) { error } val color = if (isError) TangemTheme.colors.text.warning else TangemTheme.colors.text.attention Text( text = errorText.resolveReference(), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt index 3f20ba474c..fe59dbf317 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt @@ -2,10 +2,10 @@ package com.tangem.common.ui.amountScreen.utils import androidx.compose.ui.text.input.ImeAction import com.tangem.common.ui.amountScreen.models.AmountFieldModel -import com.tangem.common.ui.amountScreen.models.MaxEnterAmount +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal -import com.tangem.utils.isNullOrZero +import com.tangem.utils.extensions.isZero import java.math.BigDecimal import java.math.RoundingMode @@ -36,7 +36,10 @@ internal fun String.getFiatValue( } } -internal fun String.checkExceedBalance(maxEnterAmount: MaxEnterAmount, amountTextField: AmountFieldModel): Boolean { +internal fun String.checkExceedBalance( + maxEnterAmount: EnterAmountBoundary, + amountTextField: AmountFieldModel, +): Boolean { val currencyCryptoAmount = maxEnterAmount.amount ?: BigDecimal.ZERO val currencyFiatAmount = maxEnterAmount.fiatAmount ?: BigDecimal.ZERO val fiatDecimal = parseToBigDecimal(amountTextField.fiatAmount.decimals) @@ -48,8 +51,8 @@ internal fun String.checkExceedBalance(maxEnterAmount: MaxEnterAmount, amountTex } } -internal fun getKeyboardAction(isExceedBalance: Boolean, decimalCryptoValue: BigDecimal) = - if (!isExceedBalance && !decimalCryptoValue.isNullOrZero()) { +internal fun getKeyboardAction(isCheckFailed: Boolean, decimalCryptoValue: BigDecimal) = + if (!isCheckFailed && !decimalCryptoValue.isZero()) { ImeAction.Done } else { ImeAction.None diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt index 8ca24539b5..858fdccb46 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt @@ -47,6 +47,14 @@ sealed class NotificationUM(val config: NotificationConfig) { ), ) + data class MinimumSendAmountError(val amount: String) : Error( + title = resourceReference(R.string.send_notification_invalid_amount_title), + subtitle = resourceReference( + R.string.transfer_notification_invalid_minimum_transaction_amount_text, + wrappedList(amount, amount), + ), + ) + data class TransactionLimitError( val cryptoCurrency: String, val utxoLimit: String, diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt index 5ba4b5f528..13b0c42b88 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt @@ -75,6 +75,20 @@ object NotificationsFactory { } } + fun MutableList.addMinimumAmountErrorNotification( + minimumSendAmount: BigDecimal?, + sendingAmount: BigDecimal, + cryptoCurrency: CryptoCurrency, + ) { + if (minimumSendAmount != null && minimumSendAmount > sendingAmount) { + add( + NotificationUM.Error.MinimumSendAmountError( + amount = minimumSendAmount.format { crypto(cryptoCurrency) }, + ), + ) + } + } + fun MutableList.addTransactionLimitErrorNotification( utxoLimit: UtxoAmountLimit?, cryptoCurrency: CryptoCurrency, @@ -179,7 +193,7 @@ object NotificationsFactory { if (isExceedsLimit) { add( NotificationUM.Error.MinimumAmountError( - amount = dustValue.parseBigDecimal(cryptoCurrencyStatus.currency.decimals), + amount = dustValue.format { crypto(cryptoCurrencyStatus.currency) }, ), ) } @@ -287,7 +301,7 @@ object NotificationsFactory { dustValue?.let { add( NotificationUM.Error.MinimumAmountError( - amount = it.parseBigDecimal(sendingCurrency.decimals), + amount = it.format { crypto(sendingCurrency) }, ), ) } diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt index 3c1d48f13b..c623358a84 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt @@ -78,8 +78,6 @@ sealed class Basic( error = error, ) - class WalletOpened : Basic(event = "Wallet Opened") - class ButtonSupport(source: AnalyticsParam.ScreensSources) : Basic( event = "Request Support", params = mapOf( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt index 951d60ddf7..88cc922696 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt @@ -7,7 +7,7 @@ import java.math.BigDecimal @JsonClass(generateAdapter = true) data class YieldDTO( @Json(name = "id") - val id: String, + val id: String?, @Json(name = "token") val token: TokenDTO?, @Json(name = "tokens") @@ -67,9 +67,9 @@ data class YieldDTO( @Json(name = "address") val address: String?, @Json(name = "status") - val status: ValidatorStatusDTO, + val status: ValidatorStatusDTO?, @Json(name = "name") - val name: String, + val name: String?, @Json(name = "image") val image: String?, @Json(name = "website") @@ -83,7 +83,7 @@ data class YieldDTO( @Json(name = "votingPower") val votingPower: Double?, @Json(name = "preferred") - val preferred: Boolean, + val preferred: Boolean?, ) { @JsonClass(generateAdapter = true) enum class ValidatorStatusDTO { diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt index 3dda78cc57..34b84bebc9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt @@ -17,7 +17,7 @@ internal object StakingStoreModule { @Provides @Singleton fun provideStakingTokensStore(): StakingYieldsStore { - return DefaultStakingYieldsStore() + return DefaultStakingYieldsStore(dataStore = RuntimeDataStore()) } @Provides diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt index d30a83e4c3..eb27aad46b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt @@ -1,16 +1,32 @@ package com.tangem.datasource.local.token import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock -internal class DefaultStakingYieldsStore : StakingYieldsStore { +internal class DefaultStakingYieldsStore( + private val dataStore: StringKeyDataStore>, +) : StakingYieldsStore { - private var yields = listOf() + private val mutex = Mutex() - override fun get(): List { - return yields + override fun get(): Flow> { + return dataStore.get(KEY) } - override fun store(items: List) { - yields = items + override suspend fun getSync(): List { + return dataStore.getSyncOrNull(KEY) ?: emptyList() + } + + override suspend fun store(items: List) { + mutex.withLock { + dataStore.store(KEY, items) + } + } + + companion object { + private const val KEY = "DefaultStakingYieldsStore" } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingYieldsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingYieldsStore.kt index dc3379969d..428aa1986c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingYieldsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingYieldsStore.kt @@ -1,10 +1,13 @@ package com.tangem.datasource.local.token import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO +import kotlinx.coroutines.flow.Flow interface StakingYieldsStore { - fun get(): List + fun get(): Flow> - fun store(items: List) + suspend fun getSync(): List + + suspend fun store(items: List) } \ No newline at end of file diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 9c2e4d268b..5e53fcf4cc 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1,6 +1,11 @@ + Token nicht in Deinem Portfolio gefunden? Überprüfe die Märkte, um es zu finden und zum Kauf hinzuzufügen + Token nicht in Deinem Portfolio gefunden? Überprüfe die Märkte, um es zu finden und zum Verkauf hinzuzufügen Wähle den Token + Token nicht in Deinem Portfolio gefunden? Überprüfe die Märkte, um es für Swaps zu finden und hinzuzufügen + Es gibt keine verfügbaren Token, die mit dem ausgewählten Token getauscht werden können. Bitte wähle einen anderen. + Kein verfügbares Paar Du möchtest erhalten Du möchtest tauschen Netzwerk wählen @@ -257,7 +262,7 @@ Abgebrochen Bestätigt Bestätigen - wird bestätigt... + Wird bestätigt... Getauscht Tauschen läuft Austauschen... @@ -448,6 +453,7 @@ Preisleistung Aufbewahrungsort Sicherheitsbewertung + Der Security Score eines Tokens ist eine Metrik, die das Sicherheitsniveau einer Blockchain oder eines Tokens anhand verschiedener Faktoren bewertet und aus den unten aufgeführten Quellen zusammengestellt wird. Soziales Gesamtangebot Die maximale Anzahl von Coins oder Tokens, die jemals für eine bestimmte Kryptowährung existieren können @@ -554,10 +560,21 @@ Zugangscode wiederherstellen Identische Karten oder Ring Zugangscode + Suche nach Land + Nicht verfügbar + Suche nach Währung + Der Kaufbetrag sollte nicht höher sein als %s + Der zu kaufende Betrag muss mindestens %s betragen + Keine verfügbaren Anbieter für diese Währung Bezahlen mit + Du kannst Deine Transaktion beim Drittanbieter %s abschließen. + Umleitung auf %s... Unsere Dienstleistungen sind in diesem Land nicht verfügbar Änder oder bestätige bitte Dein Wohnsitz wurde identifiziert als + Residenz + Bitte wähle das richtige Land aus, um korrekte Zahlungsoptionen und Dienstleistungen zu gewährleisten. + Einstellungen Über Gruppe erstellen Nach Guthaben @@ -661,7 +678,7 @@ Aufgrund der Besonderheiten des Netzes %1$s ist die Gebühr für die Überweisung des gesamten Guthabens höher. Um die Kommission zu reduzieren, Kannst du %2$s verlassen. Die Gebühr ist höher Die enthaltene Kommission übersteigt den Überweisungsbetrag, was zu einem negativen Wert führt - ungültige Menge + Ungültige Menge Der Mindestbetrag für den Versand beträgt %1$s. Bitte stell sicher, dass der Restbetrag nach dem Versand nicht unter %2$s liegt. Das Zielkonto wurde nicht erstellt. Bitte änder den zu sendenden Betrag. Der zu sendende Betrag muss mindestens %s betragen @@ -744,6 +761,8 @@ Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jede Woche. Sicher staken und wöchentliche Belohnungen verdienen. Verdiene Staking-Belohnungen + Dein verbleibender Stakingbetrag ist zu niedrig, um den Einsatz aufzuheben. Du musst mehr Staken, um den Mindestbetrag zum Aufheben des Einsatzes zu erreichen. + Niedriges Stakingguthaben Aufgrund von Netzwerkproblemen ist Staking derzeit nicht verfügbar. Bitte versuche es später erneut. Beim Staking im %1$s -Netzwerk mit einem neuen Validator werden alle zuvor eingesetzten Kryptos automatisch an diesen Validator übertragen Reinvestiert Deine verdienten Prämien in Deinen Einsatzbetrag und erhöht so den potenziellen Gewinn. @@ -788,7 +807,7 @@ Lösen der Bindungen Gelocktes unlocken Entsperren - Die Anzahl der zu stakenden Krypros muss mindesten %s betragen + Der unstaking-Betrag muss mindestens %s betragen Der Betrag übersteigt das eingesetzte Guthaben Unstaken Staking beenden @@ -815,9 +834,10 @@ Tausche mehr Token zu besseren Kursen direkt in deiner Brieftasche. Neuer Swap-Anbieter verfügbar! Der Betrag umfasst:\n- Gebühr des Dienstanbieters\n- Netzgebühr für die Rücksendung von %s von der Vermittlungsstelle an die Adresse des Nutzers. + Der Betrag beinhaltet:\n• Honorar des Dienstleisters\n• Netzwerkgebühr für das Senden %1$s von der Börse zurück an die Adresse des Benutzers. \n\n Provider-Slippage kann bis zu %2$s Der Betrag enthält die Gebühren des Dienstleisters. - Anbieter-Slippage kann bis zu %s ausfallen. - Gebühren + Der Betrag beinhaltet die Gebühr des Dienstleisters. \n\nProvider-Slippage kann bis zu %s + Information Alle dezentralen Börsen benötigen Genehmigungen, um zu verhindern, dass intelligente Verträge ohne Ihre Erlaubnis auf Ihre Geldbörse zugreifen. Smart Contracts können nicht auf Ihre Token zugreifen, wenn Sie nicht zustimmen. Indem Sie Ihre Token \"freischalten\", ermächtigen Sie den 1-Zoll-Smart-Contract, sie auszugeben. Die Miner des Netzwerks erhalten eine (von Ihnen bezahlte) Gasgebühr, um diese Aktion in der Blockchain aufzuzeichnen. Sie können Ihre Token tauschen, nachdem Sie Ihre Zustimmung gegeben haben. Genehmigen Fehler bei der Gebührenschätzung. Bitte sende dein Feedback an den Support. @@ -867,6 +887,8 @@ von: %s zu: %s Validierer: %s + Minimum %s + Der Mindesttransaktionsbetrag beträgt %1$s. Versuche es erneut Du hast dieselbe Karte oder Ring gescannt. Um ein Zwillings-Wallet zu erstellen, musst du die Karte oder Ring mit der Nummer %d scannen. Du hast die falsche Doppelkarte oder Ring gescannt. Bitte versuche eine andere Karte oder Ring @@ -1002,7 +1024,7 @@ Das Solana-Netz ist überlastet. Wenn deine Transaktion nicht innerhalb von 2 Minuten bearbeitet wird, wiederhole bitte die Transaktion. Solana Netzwerkalarm Das Solana-Netzwerk erhebt alle 2 Tage eine Miete von %1$s. Konten, die sich die Miete nicht leisten können, werden aus dem Netzwerk gelöscht. Hinterlege deinem Konto mit mehr als %2$s, um es kostenlos zu nutzen. - Einige Netzwerke sind derzeit nicht erreichbar. Bitte versuche es später erneut. + Wischen Sie nach unten, um zu aktualisieren, oder versuchen Sie es später erneut. Einige Netzwerke sind nicht erreichbar Dies ist eine Testnet-Karte. Sie kann keine Transaktionen verarbeiten und sollte nur zu Test- und Entwicklungszwecken verwendet werden. Nur für Testzwecke diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 028dbab45b..f8405d31f3 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -1,6 +1,11 @@ + ¿No encuentra el token en su billetera? Consulte los mercados para encontrarlo y añadirlo a la compra + ¿No encuentra el token en su billetera? Consulte los mercados para encontrarlo y añadirlo a la venta Elige el token + ¿No encuentras el token en tu billetera? Consulte los mercados para encontrarlo y agregarlo al intercambio + No hay tokens disponibles para intercambiar con el token seleccionado. Por favor elige otro. + No hay par disponible Desea recibir Quiere intercambiar Elige red @@ -91,7 +96,7 @@ Reclamar Reclame recompensas Cerrar - confirme + Confirme Continuar Copiar Copiar la dirección @@ -554,9 +559,20 @@ Restaurar código de acceso Tarjetas idénticas Código de acceso + Buscar por país + Indisponible + Buscar por moneda + El monto de la compra no debe ser mayor a %s + La cantidad a comprar debe ser como mínimo %s + No hay proveedores disponibles para esta moneda + Pagar con Nuestros servicios no están disponibles en este país Cámbielo o confírmelo Su residencia ha sido identificada como + Residencia + Seleccione el país correcto para garantizar opciones de pago y servicios precisos. + Ajustes + Vía Agrupar Por saldo Organizar tokens @@ -699,6 +715,7 @@ Nombre El monto del staking debe ser al menos %s El monto del staking se redondeará a %1$s TRX debido a las reglas de la red. + El monto de cancelación del staking se redondeará a %1$s TRX debido a las reglas de la red. Unstaking de la reclamación Tarifa de staking de la cuenta Una cuenta de staking es una cuenta especial donde se almacenan los tokens SOL de staking. Se crea cuando delegas sus tokens a un validador para participar en la validación de transacciones y ganar recompensas. Se cobra una pequeña tarifa por crear la cuenta de staking, que se devuelve una vez que se completa el staking. @@ -725,7 +742,7 @@ Periodo de calentamiento El tiempo permitido para activar la participación en la apuesta. La red cobrará una tarifa de aprobación de token para verificar que usted está autorizando el uso de su token para el staking. - Al utilizar la función de staking, usted acepta %1$s y %2$s del proveedor + Al utilizar la función de staking, usted acepta %1$s y %2$s del proveedor Bloqueado Migrar Native staking @@ -741,6 +758,8 @@ El staking le permite ganar %1$s. Sus recompensas por apostar llegan todas las semanas. Haga staking de forma segura y comience a ganar recompensas semanales Gane recompensas por staking + Su saldo de staking será demasiado bajo para cancelar el staking. Necesitará hacer staking más para alcanzar la cantidad mínima para cancelarlo. + Saldo de staking bajo El staking no está disponible actualmente debido a las condiciones de la red. Inténtelo de nuevo más tarde. El staking en la red %1$s con un nuevo validador transferirá automáticamente todos los fondos previamente en staking a este validador. Reinvierta las recompensas obtenidas en el monto apostado, aumentando las ganancias potenciales. @@ -774,6 +793,7 @@ Recompensas El stake está bloqueado Hacer más staking + Monto del staking Ud hace el staking de %1$s y recibirá %2$s Toque para desbloquear Pulse para desbloquear o votar @@ -784,7 +804,7 @@ Desunión Desbloquear Desbloqueando - El monto del staking debe ser al menos %s + La cantidad para el staking debe ser al menos %s El monto excede el saldo apostado Sin staking Unstaking @@ -811,7 +831,9 @@ Intercambie más tokens a mejores tasas directamente en su billetera. ¡Nuevo proveedor de intercambio disponible! El monto incluye:\n• Tarifas del proveedor de servicios\n• Tarifas de red por enviar %s desde el intercambio a la dirección del usuario. + El monto incluye:\n• tarifas del proveedor de servicios\n• tarifas de red por enviar %1$s desde el intercambio a la dirección del usuario. \n\nEl slippage del proveedor puede alcanzar el %2$s El importe incluye los honorarios del proveedor de servicios. + El importe incluye las tarifas del proveedor de servicios. \n\nEl slippage del proveedor puede alcanzar el %s Tarifa Todos los exchanges descentralizados requieren aprobaciones para evitar que los smart contracts accedan a su billetera sin su permiso. Por diseño, los smart contracts no pueden acceder a tus tokens a menos que lo apruebes. Al \"desbloquear\" sus tokens, autoriza al smart contract de 1-inch a gastarlos. Los mineros de la red reciben una tarifa de gas (pagada por voz) para registrar esta acción en la blockchain. Puede intercambiar su token después de dar la aprobación. Aprobar diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index dbd1f5f831..dba4e36c8b 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1,6 +1,11 @@ + Jeton non trouvé dans votre portefeuille ? Consultez les marchés pour le trouver et l\'ajouter à l\'achat + Jeton non trouvé dans votre portefeuille ? Consultez les marchés pour le trouver et l\'ajouter à la vente Choisissez le jeton + Jeton non trouvé dans votre portefeuille ? Consultez les marchés pour le trouver et l\'ajouter à l\'échange + Il n\'y a pas de token disponible pour échanger avec le token sélectionné. Veuillez en choisir un autre. + Aucune paire disponible Vous souhaitez recevoir Vous souhaitez échanger Choisissez le réseau @@ -554,9 +559,20 @@ Restauration du code d\'accès Cartes identiques Code d\'accès + Recherche par pays + Indisponible + Recherche par devise + Le montant de l\'achat ne doit pas dépasser %s + Le montant à acheter doit être au moins %s + Aucun fournisseur disponible pour cette devise + Payer avec Nos services ne sont pas disponibles dans ce pays Modifiez-le ou confirmez-le Votre résidence a été identifiée comme + Résidence + Veuillez sélectionner le bon pays pour garantir des options de paiement et des services précis. + Paramètres + Via Grouper Par solde Organiser les jetons @@ -699,6 +715,7 @@ Nom Le montant à staker doit être au moins %s Le montant du staking sera arrondi à %1$s TRX en raison des règles du réseau. + Le montant d\'annulation du staking sera arrondi à %1$s TRX en raison des règles du réseau. Réclamation déstakée Frais de staking du compte Un compte de staking est un compte spécial où sont stockés les jetons SOL stakés. Il est créé lorsque vous déléguez vos jetons à un validateur pour participer à la validation des transactions et gagner des récompenses. Des frais minimes sont facturés pour la création du compte de staking, qui sont restitués une fois le staking terminé. @@ -725,7 +742,7 @@ Période d\'échauffement Le temps imparti pour activer la participation au staking. Le réseau facturera des frais d’approbation de jeton pour vérifier que vous autorisez l’utilisation de votre jeton pour le jalonnement. - En utilisant la fonctionnalité de staking, vous acceptez les %1$s et %2$s du fournisseur + En utilisant la fonctionnalité de staking, vous acceptez les %1$s et %2$s du fournisseur Bloqué Migrer Native staking @@ -741,6 +758,8 @@ Le staking vous permet de gagner %1$s. Vos récompenses de staking arrivent toutes les semaines. Stakez en toute sécurité et commencez à gagner des récompenses hebdomadaires Gagnez des récompenses de staking + Votre solde stakée sera trop faible pour annuler votre staking. Vous devrez staker davantage pour atteindre le montant minimum pour annuler le staking. + Solde de staking faible L\'option de staking n\'est actuellement pas disponible en raison des conditions du réseau. Veuillez réessayer plus tard. Le staking dans le réseau %1$s avec un nouveau validateur transférera automatiquement tous les fonds précédemment stakés vers ce validateur Réinvestissez vos récompenses gagnées dans le montant que vous avez staké, augmentant ainsi vos gains potentiels. @@ -774,6 +793,7 @@ Récompenses Stake verrouillé Staker plus + Montant staké Vous stakez %1$s et recevrez %2$s Appuyez pour déverrouiller Appuyez pour déverrouiller ou voter @@ -784,7 +804,7 @@ Dissociation Débloquer Déverrouillage - Le montant à staker doit être au moins %s + Le montant à destaker doit être au moins %s Le montant dépasse le solde misé Non-staké Unstaking @@ -811,7 +831,9 @@ Échangez plus de jetons à de meilleurs taux directement dans votre portefeuille. Nouveau fournisseur d\'échange disponible ! Le montant comprend :\n• les frais du fournisseur de services\n• les frais de réseau pour l\'envoi de %s depuis l\'échange vers l\'adresse de l\'utilisateur. + Le montant comprend :\n• les frais du fournisseur de services\n• les frais de réseau pour l\'envoi de %1$s depuis l\'échange vers l\'adresse de l\'utilisateur. \n\nLe slippage du fournisseur peut atteindre %2$s Le montant comprend les frais du fournisseur de services. + Le montant comprend les frais du fournisseur de services. \n\nLe slippage du fournisseur peut atteindre %s Frais Tous les échanges décentralisés nécessitent des approbations pour empêcher les smart contracts d\'accéder à votre portefeuille sans votre permission. Par conception, les smart contracts ne peuvent pas accéder à vos jetons sans votre approbation. En « déverrouillant » vos jetons, vous autorisez le smart contract 1-inch à les dépenser. Les mineurs du réseau reçoivent des frais de gaz (payés par vous) pour enregistrer cette action sur la blockchain. Vous pouvez échanger votre jeton après avoir donné votre approbation. Approuver diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 4b823654c1..af02575293 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -1,6 +1,11 @@ + ポートフォリオにトークンが見つかりませんか?マーケットから見つけ、買付のために追加してください + ポートフォリオにトークンが見つかりませんか?マーケットから見つけ、売却のために追加してください トークンを選択 + ポートフォリオにトークンが見つかりませんか?マーケットから見つけ、スワップのために追加してください + 選択したトークンと交換できるトークンがありません。別のトークンを選択してください。 + 利用可能なペアがありません 受け取りたい 交換したい ネットワークを選択 @@ -442,6 +447,7 @@ 値動き リポジトリ セキュリティ・スコア + トークンのセキュリティ・スコアとは、ブロックチェーンやトークンのセキュリティ・レベルを様々な要因に基づいて評価する指標で、以下の情報源から集計されます。 ソーシャル 総供給量 特定の暗号資産に存在しうるコインまたはトークンの最大数 @@ -546,10 +552,21 @@ アクセスコードの復元 同一のカード アクセスコード + 国で検索 + 利用不可 + 通貨で検索 + 買付金額は%s以下にしてください + 買付金額は少なくとも%sである必要があります + この通貨で利用可能なプロバイダーはありません 支払う + サードパーティプロバイダー%sで取引を完了できます。 + %sにリダイレクトしています... この国では当社のサービスはご利用いただけません 変更または確認 あなたの住居は次のように識別されています + 住居 + 正確なお支払い方法とサービスを確保するため、正しい国を選択してください。 + 設定 経由 グループ 残高順 @@ -734,6 +751,8 @@ ステーキングにより%1$sを獲得できます。ステーキング報酬は毎週受け取れます。 安全にステーキングして、報酬を毎週獲得しましょう ステーキング報酬を獲得 + 残りのステーキング残高が少なすぎてステーキングを解除できません。最小のステーキング解除残高を満たすには、さらにステーキングする必要があります。 + ステーキング残高が低いです ネットワークの状態により、現在ステーキングはご利用いただけません。しばらくしてからもう一度お試しください。 新しいバリデーターで%1$sネットワークにステーキングすると、以前にステーキングされた資金はすべてこのバリデーターに自動的に転送されます。 獲得した報酬をステーキングに再投資し、潜在的な収益を増やします。 @@ -778,7 +797,7 @@ ステーキング解約中 ロック解除 ロック解除中 - ステーキング金額は %s 以上である必要があります + ステーキング解除する金額は少なくとも%sである必要があります 金額がステーキング残高を超えています ステーキングされていない ステーキング解除 @@ -805,8 +824,9 @@ より多くのトークンをより良いレートで、ウォレット内にて直接交換します。 新しいスワッププロバイダーが利用可能になりました! この金額には以下が含まれます:\n- サービスプロバイダーの手数料\n- 取引所からユーザーのアドレスに%s を送り返すためのネットワーク手数料。 + 金額には以下が含まれます: \n • サービス プロバイダーの手数料\n • 取引所からユーザーのアドレスに%1$sを送金するためのネットワーク手数料。 \n\nプロバイダーのスリッページは最大%2$sです この金額には、サービスプロバイダーの手数料が含まれています。 - プロバイダーのスリッページは最大%sです。 + 金額にはサービスプロバイダーの手数料が含まれます。 \n\nプロバイダーのスリッページは最大%s です 手数料 すべての分散型取引所は、スマートコントラクトがあなたの許可なくウォレットにアクセスするのを防ぐために承認を必要とします。設計上、スマートコントラクトは承認なしでトークンにアクセスできません。トークンを「ロック解除」することで、あなたは1-inchのスマートコントラクトがトークンを使うことを承認します。ネットワークのマイナーは、このアクションをブロックチェーンに記録するためのガス料金(あなたが支払う)を受け取ります。承認後、トークンを交換することができます。 承認 @@ -857,6 +877,8 @@ 送金元: %s 送金先: %s バリデーター: %s + 最小%s + 最小取引金額は%1$sです。 もう一度やり直してください 同じカードをスキャンしました。ツインウォレットを作成するには、番号%dのカードをスキャンする必要があります。 間違ったツインカードをスキャンしました。別のカードをお試しください。 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 9d3df7e413..952af02796 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -1,6 +1,8 @@ Выберите токен + Нет доступных токенов для обмена с выбранным токеном, пожалуйста, выберите другой. + Нет доступных пар Вы хотите получить Вы хотите обменять Выберите сеть @@ -311,7 +313,7 @@ Транзакция Укажите лимит доступа к выбранному токену Количество %s - Функция подтверждения необходима для предоставления другому адресу разрешения на использование определенного количества ваших токенов.По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту StakeKit разрешение использовать ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете осуществить стейкинг токена. + Функция подтверждения необходима для предоставления другому адресу разрешения на использование определенного количества ваших токенов. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту StakeKit разрешение использовать ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете осуществить стейкинг токена. Чтобы продолжить, вам необходимо разрешить смарт контракту Polygon использовать ваш %s Чтобы продолжить, вам нужно разрешить смарт-контракту %1s использовать ваш %2s Дать разрешение @@ -569,7 +571,14 @@ Восстановление кода доступа Идентичные карты Код доступа + Поиск по стране + Недоступно + Поиск по валюте + Сумма покупки не может быть больше, чем %s + Сумма покупки должна составлять минимум %s Оплата с + Наши сервисы недоступны в данной стране + Настройки Через Группы По балансу @@ -761,6 +770,7 @@ Стейкайте безопасно и начинайте получать еженедельные награды. Получите награду за стейкинг Оставшийся застейканный баланс будет слишком мал для вывода. Вам потребуется застейкать больше средств, чтобы достичь минимальной суммы для вывода. + Низкий баланс стейкинга Стейкинг временно недоступен из-за проблем в сети. Пожалуйста, попробуйте позже. Стейкинг в сети %1$s с новым валидатором автоматически переведет ваши текущие застейканные средства на него. Реинвестируйте свои заработанные награды в вашу застейканную сумму, увеличивая потенциальный доход @@ -793,6 +803,7 @@ Вознаграждения Стейкинг закрыт Застейкать еще + Застейканная сумма Вы стейкаете %1$s и будете получать награду %2$s Нажмите для разблокировки Нажмите для разблокировки @@ -830,8 +841,9 @@ Обменивайте больше токенов по лучшим курсам прямо в вашем кошельке. Новый провайдер обмена! В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя. + В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %1$s от биржи обратно на адрес пользователя \n\nПроскальзывание провайдера составляет до %2$s В сумму включена комиссия провайдера сервиса. - Проскальзывание провайдера составляет до %s. + В сумму включена комиссия провайдера сервиса. \n\nПроскальзывание провайдера составляет до %s Информация Подтверждения считаются отраслевым стандартом для всех децентрализованных бирж и защищают ваш кошелек от доступа со стороны смарт-контракта без вашего разрешения. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту 1inch разрешение тратить ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете обменять свой токен. Подтвердить @@ -925,7 +937,7 @@ Произошла непредвиденная ошибка. Сообщение ошибки: %s Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки. Неверная карта или кольцо выбрана в приложении Tangem Не удалось создать транзакцию из данных Dapp. Код: %s - Произошла непредвиденная ошибка. Код ошибки: %d. Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки. + Произошла непредвиденная ошибка. Код ошибки: %d. Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать — обратитесь в службу поддержки. Нет открытых сессий WalletConnect Упс. Нет сессий. Не удалось создать пару WalletConnect: %1$s @@ -980,7 +992,7 @@ Сумма получения не может быть менее %s Это может произойти из-за того, что провайдер временно не предоставляет обмен выбранной вами пары. Пожалуйста, подождите некоторое время и попробуйте снова. (Код %s) Выбранная пара временно недоступна - Cервис временно недоступен + Сервис временно недоступен Сумма для обмена должна быть не более %s Сумма для обмена должна быть не менее %s Пожалуйста, измените сумму для обмена @@ -1019,7 +1031,7 @@ Сеть Солана испытывает высокую нагрузку. Если Ваша транзакция не прошла в течение 2 минут, повторите её отправку. Оповещение сети Солана Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату. - Некоторые сети в настоящее время недоступны. Пожалуйста, повторите попытку позже. + Свайпните вниз для обновления или попробуйте позже. Некоторые сети недоступны Это Testnet карта. Он не может обрабатывать транзакции и используется только в целях тестирования и разработки. Только для целей тестирования diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 222dbdbe02..c3f58becbb 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -1,6 +1,11 @@ + Токен не знайдено у вашому портфелі? Перевірте Ринки, щоб знайти та додати його для покупки + Токен не знайдено у вашому портфелі? Перевірте Ринки, щоб знайти та додати його для продажу Оберіть токен + Токен не знайдено у вашому портфелі? Перевірте ринки, щоб знайти та додати його для обміну + Немає доступних токенів для обміну з обраним токеном. Будь ласка, оберіть інший. + Немає вільної пари Ви хочете отримати Ви хочете обміняти Оберіть мережу @@ -523,7 +528,7 @@ Ваша seed-фраза - слово + %d слова %d слів %d слів @@ -570,9 +575,16 @@ Відновлення коду доступу Ідентичні картки Код доступу + Пошук за країною + Недоступно + Пошук по валюті + Оплата з Наші сервіси недоступні в цій країні Змінити або підтвердити Ваше місце проживання визначено як + Будь ласка, виберіть правильну країну, щоб забезпечити точні способи оплати та послуги. + Параметри + Через Групами За балансом Сортування токенів @@ -719,9 +731,10 @@ Ім\'я Сума для стейкінгу має бути не менше %s Сума стейкінгу буде округлена до %1$s TRX відповідно до правил мережі. + Сума зняття зі стейкінгу буде округлена до %1$s TRX через мережеві правила. Зняти кошти Комісія за стейкінг-акаунт - Стейкінг-акаунт - це спеціальний рахунок, на якому зберігаються застейкані SOL токени. Він створюється, коли ви делегуєте свої токени валідатору для участі у перевірці транзакцій та отримання винагород. За створення стейкінг-акаунту стягується невелика комісія, яка повертається після завершення стейкінгу. + Стейкінг-акаунт — це спеціальний рахунок, на якому зберігаються застейкані SOL токени. Він створюється, коли ви делегуєте свої токени валідатору для участі у перевірці транзакцій та отримання винагород. За створення стейкінг-акаунту стягується невелика комісія, яка повертається після завершення стейкінгу. Процентна ставка Річний відсоток, який ви можете отримати, беручи участь у стейкінгу. APR @@ -761,10 +774,13 @@ Стейкінг дає змогу вам отримувати %1$s. Винагорода буде зараховуватися кожен тиждень. Стейкайте безпечно та почніть отримувати винагороди щотижня Отримуйте винагороду за стейкінг + Ваш баланс стейкінгу, що залишився, буде занадто низьким, щоб вивести його зі стейкінгу. Вам потрібно буде застейкати більше, щоб досягти мінімальної суми для виводу. + Низький баланс стейкінгу Стейкінг тимчасово недоступний через проблеми в мережі. Будь ласка, спробуйте пізніше. Стейкінг в мережі %1$s з новим валідатором автоматично переведе всі раніше застейкані кошти до цього валідатора Реінвестуйте зароблені винагороди у суму стейкінгу, щоб збільшити потенційний прибуток. Рестейк дозволяє вам перемістити ваші кошти від одного валідатора до іншого без необхідності виводити кошти зі стейкінгу + Ви збираєтеся застейкати весь свій баланс. Ми рекомендуємо залишити невелику суму, щоб покрити комісію мережі за зняття коштів або отримання винагороди. Розблокуйте свої кошти, щоб вивести їх зі стейкінгу. Розблокування займе %s. Ваші кошти будуть доступні для використання після закінчення 21-денного періоду розблокування. Винагорода буде отримана разом з вашими незастейканими коштами. Ваші кошти будуть доступні після %s періоду розблокування. @@ -793,6 +809,7 @@ Винагороди Стейкінг закрито Застейкати більше + Застейканий залишок Ви стейкаєте %1$s і будете отримуватиме винагороду %2$s Натисніть, щоб розблокувати Натисніть, щоб розблокувати або проголосувати @@ -803,7 +820,6 @@ Розблокування Розблокувати Розблокування - Сума для стейкінгу має бути не менше %s Сума перевищує баланс стейкінгу Вивід зі стейкінгу Зняти зі стейкінгу @@ -830,7 +846,9 @@ Обмінюйте більше токенів за вигіднішим курсом прямо у своєму гаманці. З\'явився новий провайдер обмінів! Сума включає: \n• комісію постачальника послуг\n• комісію мережі за відправлення %s з біржі назад на адресу користувача. + У суму входить:\n- комісія провайдера\n- мережева комісія за відправку %1$s з біржі назад на адресу користувача. \n\nПроскакування провайдера становить до %2$s Сума включає комісію постачальника послуг. + Сума включає комісію провайдера послуг. \n\nПроскакування провайдера до %s Комісії Всі децентралізовані біржі вимагають схвалення, щоб запобігти доступу смарт-контрактів до вашого гаманця без вашого дозволу. За задумом смарт-контракти не можуть отримати доступ до ваших токенів без вашого схвалення. \"Розблоковуючи\" свої токени, ви дозволяєте смарт-контракту 1inch витрачати ваші активи. Майнери мережі отримують плату за газ (сплачену вами), щоб зафіксувати цю дію в блокчейні. Ви можете обміняти свій токен після того, як дасте дозвіл. Підтвердити @@ -881,6 +899,7 @@ від: %s до: %s валідатор: %s + Мінімальна сума транзакції становить %1$s. Спробуйте знову Ви відсканували одну й ту саму картку. Для створення twin-гаманця вам потрібно відсканувати картку з номером %d Ви відсканували не ту twin-картку. Будь ласка, спробуйте відсканувати іншу @@ -1018,7 +1037,7 @@ Мережа Солана зазнає високого навантаження. Якщо транзакція не пройшла протягом 2 хвилин, повторіть транзакцію. Оповіщення мережі Солана Мережа Solana стягує орендну плату у розмірі %1$s кожні 2 дні. Акаунти, які не можуть дозволити собі орендну плату, видаляються з мережі. Поповніть свій рахунок на суму понад %2$s, щоб не платити орендну плату. - Деякі мережі наразі недоступні. Будь ласка, спробуйте пізніше. + Проведіть пальцем вниз для оновлення або спробуйте пізніше. Деякі мережі недоступні Це картка Testnet. Вона не може обробляти транзакції і повинна використовуватися лише для тестування та розробки. Лише для цілей тестування diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 1047741f9a..2592e85ed6 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1,6 +1,11 @@ + Token not found in your portfolio? Check the Markets to find and add it for purchase + Token not found in your portfolio? Check the Markets to find and add it for sell Choose the Token + Token not found in your portfolio? Check the Markets to find and add it for swap + There are no available tokens to swap with the selected token. Please choose another one. + No available pair You want to Receive You want to Swap Choose network @@ -448,6 +453,7 @@ Price performance Repository Security score + Security score of a token is a metric that assesses the security level of a blockchain or token based on various factors and is compiled from the sources listed below. Social Total supply The maximum number of coins or tokens that can ever exist for a particular cryptocurrency @@ -557,7 +563,12 @@ Search by country Unavailable Search by currency + The purchase amount should be no more than %s + The amount to buy must be at least %s + No available providers for this currency Pay with + You will be able to complete your transaction on the third-party provider, %s + Redirecting to %s... Our services are not available in this country Change or confirm it Your residence has been identified as @@ -823,8 +834,9 @@ Exchange more tokens at better rates directly in your wallet. New Swap Provider Available! The amount includes:\n• service provider\'s fee\n• network fee for sending %s from the exchange back to the user\'s address. + The amount includes:\n• service provider\'s fee\n• network fee for sending %1$s from the exchange back to the user\'s address. \n\nProvider slippage is up to %2$s The amount includes the service provider\'s fee. - Provider slippage is up to %s. + The amount includes the service provider\'s fee. \n\nProvider slippage is up to %s Information All decentralized exchanges require approvals to prevent smart contracts from accessing your wallet without your permission. By design, smart contracts can\'t access your tokens unless you approve. By \"unlocking\" your tokens, you authorize the 1-inch smart contract to spend them. The network\'s miners receive a gas fee (paid by you) to record this action on the blockchain. You can swap your token after giving approval. Approve @@ -875,6 +887,7 @@ from: %s to: %s validator: %s + Minimum %s The minimum transaction amount is %1$s. Try again You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d @@ -1011,7 +1024,7 @@ The Solana network is congested. If your transaction is not processed within 2 minutes, please repeat the transaction. Solana Network Alert Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free. - Some networks currently are unreachable. Please try again later. + Swipe down to refresh or try again later. Some networks are unreachable This is a Testnet card. It cannot process transactions and should only be used for testing and development purposes. For testing purposes only diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt index eea3130209..7d6849591c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt @@ -5,6 +5,7 @@ import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CRYPTO_FEE import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CURRENCY_SPACE import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.FORMAT_THRESHOLD import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.utils.StringsSigns.NON_BREAKING_SPACE import com.tangem.utils.extensions.isNotWhitespace import java.math.BigDecimal import java.math.RoundingMode @@ -16,6 +17,7 @@ open class BigDecimalCryptoFormat( val symbol: String, val decimals: Int, val locale: Locale = Locale.getDefault(), + val ignoreSymbolPosition: Boolean = false, ) : BigDecimalFormat { override fun invoke(value: BigDecimal): String = defaultAmount()(value) @@ -48,11 +50,13 @@ fun BigDecimalFormatScope.crypto( fun BigDecimalFormatScope.crypto( cryptoCurrency: CryptoCurrency, + ignoreSymbolPosition: Boolean = false, locale: Locale = Locale.getDefault(), ): BigDecimalCryptoFormat { return BigDecimalCryptoFormat( symbol = cryptoCurrency.symbol, decimals = cryptoCurrency.decimals, + ignoreSymbolPosition = ignoreSymbolPosition, locale = locale, ) } @@ -60,19 +64,28 @@ fun BigDecimalFormatScope.crypto( // == Formatters == fun BigDecimalCryptoFormat.defaultAmount() = BigDecimalFormat { value -> - val formatter = NumberFormat.getCurrencyInstance(locale).apply { - currency = usdCurrency - maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) - minimumFractionDigits = 2 - isGroupingUsed = true - roundingMode = RoundingMode.HALF_UP + if (ignoreSymbolPosition) { + val formatter = NumberFormat.getInstance(locale).apply { + maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + formatter.format(value) + NON_BREAKING_SPACE + symbol + } else { + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = usdCurrency + maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + formatter.format(value) + .replaceFiatSymbolWithCrypto( + fiatCurrencySymbol = usdCurrency.getSymbol(locale), + cryptoCurrencySymbol = symbol, + ) } - - formatter.format(value) - .replaceFiatSymbolWithCrypto( - fiatCurrencySymbol = usdCurrency.getSymbol(locale), - cryptoCurrencySymbol = symbol, - ) } fun BigDecimalCryptoFormat.shorted() = BigDecimalFormat { value -> diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/BigDecimalExt.kt b/core/utils/src/main/java/com/tangem/utils/extensions/BigDecimalExt.kt index 31a31d6704..45f2966872 100644 --- a/core/utils/src/main/java/com/tangem/utils/extensions/BigDecimalExt.kt +++ b/core/utils/src/main/java/com/tangem/utils/extensions/BigDecimalExt.kt @@ -12,4 +12,7 @@ fun BigDecimal.isZero(): Boolean = this.compareTo(BigDecimal.ZERO) == 0 fun BigDecimal.isPositive(): Boolean = this.signum() == 1 /** Removes trailing zeros and returns plain [String] */ -fun BigDecimal.stripZeroPlainString(): String = this.stripTrailingZeros().toPlainString() \ No newline at end of file +fun BigDecimal.stripZeroPlainString(): String = this.stripTrailingZeros().toPlainString() + +/** Compares two [BigDecimal] numbers */ +infix fun BigDecimal.isEqualTo(other: BigDecimal): Boolean = this.compareTo(other) == 0 \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 9abafd796c..00a9d68849 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -55,7 +55,9 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.plus import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull import timber.log.Timber +import kotlin.time.Duration.Companion.seconds @Suppress("LargeClass", "LongParameterList", "TooManyFunctions") internal class DefaultStakingRepository( @@ -131,7 +133,7 @@ internal class DefaultStakingRepository( val rawCurrencyId = cryptoCurrencyId.rawCurrencyId ?: error("Staking custom tokens is not available") val prefetchedYield = findPrefetchedYield( - yields = getEnabledYields(), + yields = getEnabledYieldsSync(), currencyId = rawCurrencyId, symbol = symbol, ) @@ -186,7 +188,7 @@ internal class DefaultStakingRepository( } } - override fun getStakingAvailability( + override suspend fun getStakingAvailability( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, ): StakingAvailability { @@ -197,7 +199,7 @@ internal class DefaultStakingRepository( val isSupportedInMobileApp = getSupportedIntegrationId(cryptoCurrency.id).isNullOrEmpty().not() val prefetchedYield = findPrefetchedYield( - yields = getEnabledYields(), + yields = getEnabledYieldsSync(), currencyId = rawCurrencyId, symbol = cryptoCurrency.symbol, ) @@ -406,12 +408,22 @@ internal class DefaultStakingRepository( key = getYieldBalancesKey(userWalletId), skipCache = refresh, block = { - val yields = getEnabledYields().ifEmpty { + val yieldDTOs = withTimeoutOrNull(YIELDS_WATITING_TIMEOUT) { + runCatching { stakingYieldsStore.get().firstOrNull() }.getOrNull() + } + + if (yieldDTOs == null) { Timber.i("No enabled yields for $userWalletId") stakingBalanceStore.store(userWalletId, emptySet()) return@invokeOnExpire } + + val yields = yieldConverter.convertListIgnoreErrors( + input = yieldDTOs, + onError = { Timber.e("Error converting one of the items in enabled yields: $it") }, + ) + val availableCurrencies = cryptoCurrencies .mapNotNull { currency -> val addresses = walletManagersFacade.getAddresses(userWalletId, currency.network) @@ -428,7 +440,6 @@ internal class DefaultStakingRepository( } .map { getBalanceRequestData(it.first.value, it.second) } .ifEmpty { - Timber.i("No yield balances available for $userWalletId") stakingBalanceStore.store(userWalletId, emptySet()) cacheRegistry.invalidate(getYieldBalancesKey(userWalletId)) @@ -564,10 +575,10 @@ internal class DefaultStakingRepository( } } - private fun getEnabledYields(): List { + private suspend fun getEnabledYieldsSync(): List { return yieldConverter.convertListIgnoreErrors( - input = stakingYieldsStore.get(), - onError = { Timber.e("Error converting enabled yields list: $it") }, + input = stakingYieldsStore.getSync(), + onError = { Timber.e("Error converting one of the items in enabled yields: $it") }, ) } @@ -614,6 +625,8 @@ internal class DefaultStakingRepository( const val ETHEREUM_POLYGON_APPROVE_SPENDER = "0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908" + val YIELDS_WATITING_TIMEOUT = 15.seconds + val INVALID_BATCHES_FOR_SOLANA = listOf("AC01", "CB79") // uncomment items as implementation is ready diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt index 5edd7f05e7..c0807165b7 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt @@ -31,10 +31,12 @@ internal class YieldBalanceConverter : Converter { - TxHistoryItem.TransactionType.TronStakingTransactionType.Stake + TxHistoryItem.TransactionType.Staking.Stake } is TransactionType.TronStakingTransactionType.UnfreezeBalanceV2Contract -> { - TxHistoryItem.TransactionType.TronStakingTransactionType.Unstake + TxHistoryItem.TransactionType.Staking.Unstake } is TransactionType.TronStakingTransactionType.VoteWitnessContract -> { - TxHistoryItem.TransactionType.TronStakingTransactionType.Vote(value.validatorAddress) + TxHistoryItem.TransactionType.Staking.Vote(value.validatorAddress) } is TransactionType.TronStakingTransactionType.WithdrawBalanceContract -> { - TxHistoryItem.TransactionType.TronStakingTransactionType.ClaimRewards + TxHistoryItem.TransactionType.Staking.ClaimRewards } is TransactionType.TronStakingTransactionType.WithdrawExpireUnfreezeContract -> { - TxHistoryItem.TransactionType.TronStakingTransactionType.Withdraw + TxHistoryItem.TransactionType.Staking.Withdraw } } } @@ -43,6 +43,22 @@ internal class SdkTransactionTypeConverter( "transfer" -> TxHistoryItem.TransactionType.Transfer "approve" -> TxHistoryItem.TransactionType.Approve "swap" -> TxHistoryItem.TransactionType.Swap + "buyVoucher", + "buyVoucherPOL", + "delegate", + -> TxHistoryItem.TransactionType.Staking.Stake + "sellVoucher_new", + "sellVoucher_newPOL", + "undelegate", + -> TxHistoryItem.TransactionType.Staking.Unstake + "unstakeClaimTokens_new", + "unstakeClaimTokens_newPOL", + "claim", + -> TxHistoryItem.TransactionType.Staking.Withdraw + "withdrawRewards", + "withdrawRewardsPOL", + -> TxHistoryItem.TransactionType.Staking.ClaimRewards + "redelegate" -> TxHistoryItem.TransactionType.Staking.Restake null -> TxHistoryItem.TransactionType.UnknownOperation else -> TxHistoryItem.TransactionType.Operation(name = methodName.replaceFirstChar { it.titlecase() }) } diff --git a/domain/staking/build.gradle.kts b/domain/staking/build.gradle.kts index 9832ec7010..a0caf393f8 100644 --- a/domain/staking/build.gradle.kts +++ b/domain/staking/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { api(projects.domain.staking.models) api(projects.domain.core) api(projects.core.analytics) + api(projects.core.utils) implementation(deps.kotlin.serialization) implementation(deps.jodatime) diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionType.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionType.kt index 3d449ae82a..01e02cea2f 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionType.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionType.kt @@ -25,9 +25,10 @@ enum class StakingActionType { UNSTAKE -> "Unstake" CLAIM_REWARDS -> "Claim Rewards" RESTAKE_REWARDS -> "Restake Rewards" - WITHDRAW -> "Withdraw" + CLAIM_UNSTAKED, + WITHDRAW, + -> "Withdraw" RESTAKE -> "Restake" - CLAIM_UNSTAKED -> "Claim Unstaked" UNLOCK_LOCKED -> "Unlock Locked" STAKE_LOCKED -> "Stake Locked" VOTE -> "Vote" diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt index b2ab9c2b1e..7133219a03 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt @@ -16,7 +16,7 @@ class GetStakingAvailabilityUseCase( private val stakingErrorResolver: StakingErrorResolver, ) { - operator fun invoke( + suspend operator fun invoke( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, ): Either { diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt index 1b0f6b9128..29e7148ee2 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt @@ -7,6 +7,8 @@ import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.repositories.StakingErrorResolver +import com.tangem.utils.extensions.isEqualTo +import java.math.BigDecimal import java.util.UUID class InvalidatePendingTransactionsUseCase( @@ -36,8 +38,12 @@ class InvalidatePendingTransactionsUseCase( processingActions.forEach { action -> when (action.type) { - StakingActionType.STAKE, StakingActionType.VOTE, StakingActionType.VOTE_LOCKED -> { - processEnterAction(balances, action) + StakingActionType.STAKE, StakingActionType.VOTE -> { + addStubStakedPendingTransaction(balances, action) + } + StakingActionType.VOTE_LOCKED -> { + addStubStakedPendingTransaction(balances, action) + removeLockedBalance(balances, action) } StakingActionType.WITHDRAW -> { modifyBalancesByStatus(balances, action, BalanceType.UNSTAKED) @@ -45,9 +51,15 @@ class InvalidatePendingTransactionsUseCase( StakingActionType.UNLOCK_LOCKED -> { modifyBalancesByStatus(balances, action, BalanceType.LOCKED) } - StakingActionType.UNSTAKE -> { + StakingActionType.RESTAKE -> { modifyBalancesByStatus(balances, action, BalanceType.STAKED) } + StakingActionType.UNSTAKE -> { + val isFullUnstake = modifyBalancesByStatus(balances, action, BalanceType.STAKED) + if (!isFullUnstake) { + processPartialUnstake(balances, action) + } + } else -> { // intentionally do nothing } @@ -57,7 +69,15 @@ class InvalidatePendingTransactionsUseCase( return balances } - private fun processEnterAction(balances: MutableList, action: StakingAction) { + private fun removeLockedBalance(balances: MutableList, action: StakingAction) { + val index = findBalanceIndex(balances, action, BalanceType.LOCKED) + + if (index != -1) { + balances.removeAt(index) + } + } + + private fun addStubStakedPendingTransaction(balances: MutableList, action: StakingAction) { balances.add( BalanceItem( groupId = UUID.randomUUID().toString(), @@ -73,13 +93,51 @@ class InvalidatePendingTransactionsUseCase( ) } - private fun modifyBalancesByStatus(balances: MutableList, action: StakingAction, type: BalanceType) { - val index = balances.indexOfFirst { - !it.isPending && it.amount == action.amount && it.type == type - } + private fun modifyBalancesByStatus( + balances: MutableList, + action: StakingAction, + type: BalanceType, + ): Boolean { + val index = findBalanceIndex(balances, action, type) if (index != -1) { balances[index] = balances[index].copy(isPending = true) + return true + } + + return false + } + + private fun findBalanceIndex(balances: MutableList, action: StakingAction, type: BalanceType): Int { + return balances.indexOfFirst { + !it.isPending && it.amount isEqualTo action.amount && it.type == type } } + + private fun processPartialUnstake(balances: MutableList, action: StakingAction) { + val (index, pendingActionAmount) = findPartialUnstake(balances, action) + + if (index != -1) { + val amount = balances[index].amount + balances[index] = balances[index].copy( + amount = amount - pendingActionAmount, + ) // remnants of real one + + balances.add( + balances[index].copy( + amount = pendingActionAmount, + isPending = true, + ), + ) // pending with amount from action + } + } + + private fun findPartialUnstake(balances: MutableList, action: StakingAction): Pair { + val index = balances.indexOfFirst { + !it.isPending && action.amount < it.amount && + it.type == BalanceType.STAKED && + it.validatorAddress == action.validatorAddress + } + return index to action.amount + } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index e68adf73a9..cc53ade06f 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -33,7 +33,7 @@ interface StakingRepository { suspend fun getYield(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Yield - fun getStakingAvailability(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): StakingAvailability + suspend fun getStakingAvailability(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): StakingAvailability suspend fun getActions( userWalletId: UserWalletId, 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 index 780c6c48fe..dae8238b02 100644 --- 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 @@ -6,6 +6,7 @@ import java.math.BigDecimal data class CryptoCurrencyCheck( val dustValue: BigDecimal?, val reserveAmount: BigDecimal?, + val minimumSendAmount: BigDecimal?, val existentialDeposit: BigDecimal?, val utxoAmountLimit: UtxoAmountLimit?, val isAccountFunded: Boolean, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index ff6b9ec8d0..2e91b110df 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -298,7 +298,7 @@ class GetCryptoCurrencyActionsUseCase( return networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty() } - private fun isStakingAvailable(userWallet: UserWallet, cryptoCurrency: CryptoCurrency): Boolean { + private suspend fun isStakingAvailable(userWallet: UserWallet, cryptoCurrency: CryptoCurrency): Boolean { return stakingRepository.getStakingAvailability( userWalletId = userWallet.walletId, cryptoCurrency = cryptoCurrency, 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 index aa488b2671..ba9e837248 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt @@ -24,6 +24,7 @@ class GetCurrencyCheckUseCase( val network = currencyStatus.currency.network val dustValue = currencyChecksRepository.getDustValue(userWalletId, network) val reserveAmount = currencyChecksRepository.getReserveAmount(userWalletId, network) + val minimumSendAmount = currencyChecksRepository.getMinimumSendAmount(userWalletId, network) val existentialDeposit = currencyChecksRepository.getExistentialDeposit(userWalletId, network) val isAccountFunded = recipientAddress?.let { currencyChecksRepository.checkIfAccountFunded( @@ -46,6 +47,7 @@ class GetCurrencyCheckUseCase( CryptoCurrencyCheck( dustValue = dustValue, reserveAmount = reserveAmount, + minimumSendAmount = minimumSendAmount, existentialDeposit = existentialDeposit, utxoAmountLimit = utxoAmountLimit, isAccountFunded = isAccountFunded, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMinimumTransactionAmountSyncUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMinimumTransactionAmountSyncUseCase.kt new file mode 100644 index 0000000000..388b88d7e7 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMinimumTransactionAmountSyncUseCase.kt @@ -0,0 +1,21 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.raise.either +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.repository.CurrencyChecksRepository +import com.tangem.domain.wallets.models.UserWalletId +import java.math.BigDecimal + +class GetMinimumTransactionAmountSyncUseCase( + private val currencyChecksRepository: CurrencyChecksRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): Either = either { + val cryptoCurrency = cryptoCurrencyStatus.currency + currencyChecksRepository.getMinimumSendAmount(userWalletId, cryptoCurrency.network) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrencyChecksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrencyChecksRepository.kt index 0b5745a983..4ff37c6420 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrencyChecksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrencyChecksRepository.kt @@ -20,6 +20,9 @@ interface CurrencyChecksRepository { /** Returns reserve amount which is required to create an account */ suspend fun getReserveAmount(userWalletId: UserWalletId, network: Network): BigDecimal? + /** Returns minimum send transaction amount */ + suspend fun getMinimumSendAmount(userWalletId: UserWalletId, network: Network): BigDecimal? + /** Returns a fee resource amount available and max for paying fees in several blockchains */ suspend fun getFeeResourceAmount(userWalletId: UserWalletId, network: Network): CurrencyAmount? diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index fa3e6119d7..b66b4ccaf9 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -112,7 +112,7 @@ class MockStakingRepository : StakingRepository { isAvailable = false, ) - override fun getStakingAvailability( + override suspend fun getStakingAvailability( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, ): StakingAvailability = StakingAvailability.Unavailable diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt index 134cb9f3f5..8edb20c7a6 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt @@ -15,8 +15,6 @@ import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.error.mapToFeeError import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow import java.math.BigDecimal /** @@ -30,28 +28,25 @@ class EstimateFeeUseCase( amount: BigDecimal, userWallet: UserWallet, cryptoCurrency: CryptoCurrency, - ): Flow> { - return flow { - val amountData = convertCryptoCurrencyToAmount(cryptoCurrency, amount) - val result = if (demoConfig.isDemoCardId(userWallet.scanResponse.card.cardId)) { - demoTransactionSender(userWallet, cryptoCurrency).estimateFee( - amount = amountData, - destination = "", - ) - } else { - walletManagersFacade.estimateFee( - amount = amountData, - userWalletId = userWallet.walletId, - network = cryptoCurrency.network, - ) - } + ): Either { + val amountData = convertCryptoCurrencyToAmount(cryptoCurrency, amount) + val result = if (demoConfig.isDemoCardId(userWallet.scanResponse.card.cardId)) { + demoTransactionSender(userWallet, cryptoCurrency).estimateFee( + amount = amountData, + destination = "", + ) + } else { + walletManagersFacade.estimateFee( + amount = amountData, + userWalletId = userWallet.walletId, + network = cryptoCurrency.network, + ) + } - val maybeFee = when (result) { - is Result.Success -> result.data.right() - is Result.Failure -> result.mapToFeeError().left() - null -> GetFeeError.UnknownError.left() - } - emit(maybeFee) + return when (result) { + is Result.Success -> result.data.right() + is Result.Failure -> result.mapToFeeError().left() + null -> GetFeeError.UnknownError.left() } } diff --git a/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt b/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt index f96905a1ed..61f2278715 100644 --- a/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt +++ b/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt @@ -40,12 +40,13 @@ data class TxHistoryItem( data object UnknownOperation : TransactionType data class Operation(val name: String) : TransactionType - sealed interface TronStakingTransactionType : TransactionType { - data class Vote(val validatorAddress: String) : TronStakingTransactionType - data object ClaimRewards : TronStakingTransactionType - data object Stake : TronStakingTransactionType - data object Unstake : TronStakingTransactionType - data object Withdraw : TronStakingTransactionType + sealed interface Staking : TransactionType { + data class Vote(val validatorAddress: String) : Staking + data object ClaimRewards : Staking + data object Stake : Staking + data object Unstake : Staking + data object Withdraw : Staking + data object Restake : Staking } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index 0b69329371..030e8e81c7 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -44,7 +44,7 @@ internal class SendStateFactory( appCurrencyProvider = appCurrencyProvider, iconStateConverter = iconStateConverter, cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, - maxEnterAmountProvider = Provider { maxEnterAmountConverter.convert(cryptoCurrencyStatusProvider()) }, + maxEnterAmount = maxEnterAmountConverter.convert(cryptoCurrencyStatusProvider()), ) } private val recipientStateConverter by lazy(LazyThreadSafetyMode.NONE) { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt index 204918b99b..9b8d11fb41 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.impl.presentation.state.amount import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter @@ -16,6 +17,7 @@ internal class AmountStateFactory( private val stateRouterProvider: Provider, private val currentStateProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, + private val minimumTransactionAmountProvider: Provider, ) { private val amountFieldChangeConverter by lazy(LazyThreadSafetyMode.NONE) { @@ -23,6 +25,7 @@ internal class AmountStateFactory( stateRouterProvider = stateRouterProvider, currentStateProvider = currentStateProvider, cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + minimumTransactionAmountProvider = minimumTransactionAmountProvider, ) } private val amountFieldMaxAmountConverter by lazy(LazyThreadSafetyMode.NONE) { @@ -30,6 +33,7 @@ internal class AmountStateFactory( stateRouterProvider = stateRouterProvider, currentStateProvider = currentStateProvider, cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + minimumTransactionAmountProvider = minimumTransactionAmountProvider, ) } @@ -51,6 +55,7 @@ internal class AmountStateFactory( stateRouterProvider = stateRouterProvider, currentStateProvider = currentStateProvider, cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + minimumTransactionAmountProvider = minimumTransactionAmountProvider, ) } private val amountReduceToConverter by lazy { @@ -58,6 +63,7 @@ internal class AmountStateFactory( stateRouterProvider = stateRouterProvider, currentStateProvider = currentStateProvider, cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + minimumTransactionAmountProvider = minimumTransactionAmountProvider, ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceByConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceByConverter.kt index 0468cd8c82..c0e28102e7 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceByConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceByConverter.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.impl.presentation.state.amount import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter @@ -11,6 +12,7 @@ internal class SendAmountReduceByConverter( private val stateRouterProvider: Provider, private val currentStateProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, + private val minimumTransactionAmountProvider: Provider, ) : Converter { override fun convert(value: AmountReduceByTransformer.ReduceByData): SendUiState { @@ -23,7 +25,11 @@ internal class SendAmountReduceByConverter( sendState = state.sendState?.copy( reduceAmountBy = value.reduceAmountBy, ), - amountState = AmountReduceByTransformer(cryptoCurrencyStatusProvider(), value).transform(amountState), + amountState = AmountReduceByTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatusProvider(), + minimumTransactionAmount = minimumTransactionAmountProvider(), + value = value, + ).transform(amountState), ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceToConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceToConverter.kt index 1bb5518063..ea1caaf180 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceToConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceToConverter.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.impl.presentation.state.amount import com.tangem.common.ui.amountScreen.converters.AmountReduceToTransformer +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter @@ -12,6 +13,7 @@ internal class SendAmountReduceToConverter( private val stateRouterProvider: Provider, private val currentStateProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, + private val minimumTransactionAmountProvider: Provider, ) : Converter { override fun convert(value: BigDecimal): SendUiState { @@ -21,7 +23,11 @@ internal class SendAmountReduceToConverter( return state.copyWrapped( isEditState = isEditState, - amountState = AmountReduceToTransformer(cryptoCurrencyStatusProvider(), value).transform(amountState), + amountState = AmountReduceToTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatusProvider(), + minimumTransactionAmount = minimumTransactionAmountProvider(), + value = value, + ).transform(amountState), ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index bfe558339a..9975712433 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -11,6 +11,7 @@ import com.tangem.common.ui.notifications.NotificationsFactory.addExceedsBalance 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.addMinimumAmountErrorNotification import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountErrorNotification import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification import com.tangem.common.ui.notifications.NotificationsFactory.addValidateTransactionNotifications @@ -192,6 +193,11 @@ internal class SendNotificationFactory( cryptoCurrency = currency, isAccountFunded = currencyCheck.isAccountFunded, ) + addMinimumAmountErrorNotification( + minimumSendAmount = currencyCheck.minimumSendAmount, + sendingAmount = sendingAmount, + cryptoCurrency = currency, + ) } private suspend fun MutableList.addWarningNotifications( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt index c4f28c469e..ea7e502aec 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt @@ -2,6 +2,7 @@ package com.tangem.features.send.impl.presentation.state.fields import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter @@ -12,21 +13,29 @@ internal class SendAmountFieldChangeConverter( private val stateRouterProvider: Provider, private val currentStateProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, + private val minimumTransactionAmountProvider: Provider, ) : Converter { private val maxEnterAmountConverter = MaxEnterAmountConverter() override fun convert(value: String): SendUiState { val state = currentStateProvider() + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val isEditState = stateRouterProvider().isEditState val amountState = state.getAmountState(isEditState) - val maxEnterAmount = maxEnterAmountConverter.convert(cryptoCurrencyStatusProvider()) + val maxEnterAmount = maxEnterAmountConverter.convert(cryptoCurrencyStatus) + val minimumTransactionAmount = minimumTransactionAmountProvider() return state.copyWrapped( isEditState = isEditState, sendState = state.sendState?.copy(reduceAmountBy = null), - amountState = AmountFieldChangeTransformer(maxEnterAmount, value).transform(amountState), + amountState = AmountFieldChangeTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + maxEnterAmount = maxEnterAmount, + minimumTransactionAmount = minimumTransactionAmount, + value = value, + ).transform(amountState), ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt index 6e071d34d8..882365d726 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt @@ -2,6 +2,7 @@ package com.tangem.features.send.impl.presentation.state.fields import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter import com.tangem.common.ui.amountScreen.converters.field.AmountFieldSetMaxAmountTransformer +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter @@ -13,6 +14,7 @@ internal class SendAmountFieldMaxAmountConverter( private val stateRouterProvider: Provider, private val currentStateProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, + private val minimumTransactionAmountProvider: Provider, ) : Converter { private val maxEnterAmountConverter = MaxEnterAmountConverter() @@ -27,11 +29,16 @@ internal class SendAmountFieldMaxAmountConverter( if (decimalCryptoValue.isNullOrZero()) return state val maxEnterAmount = maxEnterAmountConverter.convert(cryptoCurrencyStatus) + val minimumTransactionAmount = minimumTransactionAmountProvider() return state.copyWrapped( isEditState = isEditState, sendState = state.sendState?.copy(reduceAmountBy = null), - amountState = AmountFieldSetMaxAmountTransformer(maxEnterAmount).transform(amountState), + amountState = AmountFieldSetMaxAmountTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + maxAmount = maxEnterAmount, + minAmount = minimumTransactionAmount, + ).transform(amountState), ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 468482bb95..14b94f1d08 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -12,6 +12,7 @@ import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.routing.AppRoute import com.tangem.common.routing.bundle.unbundle import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.utils.parseBigDecimal @@ -59,6 +60,7 @@ import com.tangem.features.send.impl.presentation.state.recipient.RecipientSendF import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.Provider import com.tangem.utils.coroutines.* +import com.tangem.utils.extensions.orZero import com.tangem.utils.extensions.stripZeroPlainString import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.* @@ -78,6 +80,7 @@ internal class SendViewModel @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getWalletsUseCase: GetWalletsUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, + private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase, private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, private val getNetworkAddressesUseCase: GetNetworkAddressesUseCase, private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, @@ -153,6 +156,7 @@ internal class SendViewModel @Inject constructor( stateRouterProvider = Provider { stateRouter }, currentStateProvider = Provider { uiState.value }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + minimumTransactionAmountProvider = Provider { minimumTransactionAmount }, ) private val feeStateFactory = FeeStateFactory( @@ -212,6 +216,7 @@ internal class SendViewModel @Inject constructor( private var isTapHelpPreviewEnabled: Boolean = false private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var feeCryptoCurrencyStatus: CryptoCurrencyStatus? = null + private var minimumTransactionAmount: EnterAmountBoundary? = null private var balanceJobHolder = JobHolder() private var balanceHidingJobHolder = JobHolder() @@ -295,6 +300,7 @@ internal class SendViewModel @Inject constructor( onDataLoaded( currencyStatus = cryptoCurrencyStatus, feeCurrencyStatus = getFeeCurrencyStatusSync(cryptoCurrencyStatus, isMultiCurrency), + minTransactionAmount = getMinimumTransactionAmount(cryptoCurrencyStatus), ) }, ifLeft = { showErrorAlert() }, @@ -336,6 +342,18 @@ internal class SendViewModel @Inject constructor( } } + private suspend fun getMinimumTransactionAmount(cryptoCurrencyStatus: CryptoCurrencyStatus): EnterAmountBoundary? { + return getMinimumTransactionAmountSyncUseCase( + userWalletId = userWalletId, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ).getOrNull()?.let { + EnterAmountBoundary( + amount = it, + fiatRate = cryptoCurrencyStatus.value.fiatRate.orZero(), + ) + } + } + private fun createSelectedAppCurrencyFlow(): StateFlow { return getSelectedAppCurrencyUseCase() .map { maybeAppCurrency -> @@ -348,9 +366,14 @@ internal class SendViewModel @Inject constructor( ) } - private fun onDataLoaded(currencyStatus: CryptoCurrencyStatus, feeCurrencyStatus: CryptoCurrencyStatus?) { + private fun onDataLoaded( + currencyStatus: CryptoCurrencyStatus, + feeCurrencyStatus: CryptoCurrencyStatus?, + minTransactionAmount: EnterAmountBoundary?, + ) { cryptoCurrencyStatus = currencyStatus feeCryptoCurrencyStatus = feeCurrencyStatus + minimumTransactionAmount = minTransactionAmount subscribeOnQRScannerResult() when { uiState.value.sendState?.isSuccess == true -> return diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt index 114d86d579..5decf65514 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt @@ -18,7 +18,7 @@ internal class StakingAnalyticSender( fun initialInfoScreen(value: StakingUiState) { val initialInfoState = value.initialInfoState as? StakingStates.InitialInfoState.Data val validatorState = initialInfoState?.yieldBalance as? InnerYieldBalanceState.Data - val validatorCount = validatorState?.balance + val validatorCount = validatorState?.balances ?.filterNot { it.validator?.address.isNullOrBlank() } ?.distinctBy { it.validator?.address } ?.size ?: 0 diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt index 7fa7d42377..94cd29f646 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt @@ -16,7 +16,7 @@ internal sealed class InnerYieldBalanceState { val rewardsFiat: String, val rewardBlockType: RewardBlockType, val isActionable: Boolean, - val balance: ImmutableList, + val balances: ImmutableList, ) : InnerYieldBalanceState() data object Empty : InnerYieldBalanceState() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt index 5b02449fa4..d9fd203280 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt @@ -39,7 +39,7 @@ internal object StakingNotification { ) : NotificationUM.Warning( title = title, subtitle = subtitle, - iconResId = R.drawable.ic_alert_circle_24, + iconResId = R.drawable.img_attention_20, buttonsState = buttonsState, onCloseClick = onCloseClick, ) { @@ -47,6 +47,11 @@ internal object StakingNotification { val title: TextReference, val description: TextReference, ) : StakingNotification.Warning(title = title, subtitle = description) + + data object LowStakedBalance : StakingNotification.Warning( + title = resourceReference(R.string.staking_notification_low_staked_balance_title), + subtitle = resourceReference(R.string.staking_notification_low_staked_balance_text), + ) } sealed class Info( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index c30d7b9185..e66c38a778 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -75,7 +75,7 @@ internal class StakingStateController @Inject constructor( walletName = "", cryptoCurrencyName = "", cryptoCurrencySymbol = "", - cryptoCurrencyNetworkId = "", + cryptoCurrencyBlockchainId = "", currentStep = StakingStep.InitialInfo, initialInfoState = StakingStates.InitialInfoState.Empty(), amountState = AmountState.Empty(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt index 48823d364f..d7e8d24c25 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -26,7 +26,7 @@ internal class StakingStateRouter( StakingStep.InitialInfo -> when (stateController.value.actionType) { StakingActionCommonType.Enter -> showAmount() // TODO staking [REDACTED_TASK_KEY] support solana multisize hashes signing - StakingActionCommonType.Exit -> if (isSolana(stateController.value.cryptoCurrencyNetworkId)) { + StakingActionCommonType.Exit -> if (isSolana(stateController.value.cryptoCurrencyBlockchainId)) { showConfirmation() } else { showAmount() @@ -54,10 +54,15 @@ internal class StakingStateRouter( StakingStep.Amount, -> showInitial() StakingStep.Confirmation -> { - if (uiState.actionType != StakingActionCommonType.Enter) { - showInitial() - } else { + val isEnter = uiState.actionType == StakingActionCommonType.Enter + val isExit = uiState.actionType == StakingActionCommonType.Exit + + // TODO staking [REDACTED_TASK_KEY] support solana multisize hashes signing + val isSolana = isSolana(uiState.cryptoCurrencyBlockchainId) + if (isEnter || isExit && !isSolana) { showAmount() + } else { + showInitial() } } StakingStep.Validators -> showConfirmation() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 470048621d..c8c0be08e6 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -29,7 +29,7 @@ internal data class StakingUiState( val walletName: String, val cryptoCurrencyName: String, val cryptoCurrencySymbol: String, - val cryptoCurrencyNetworkId: String, + val cryptoCurrencyBlockchainId: String, val currentStep: StakingStep, val initialInfoState: StakingStates.InitialInfoState, val amountState: AmountState, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index 29ccbc1d29..41c9881df8 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -53,7 +53,7 @@ internal class YieldBalancesConverter( }, rewardBlockType = type, isActionable = isActionable, - balance = balanceToShowItems.mapBalances(), + balances = balanceToShowItems.mapBalances(), ) } else { InnerYieldBalanceState.Empty diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt index e10d32b8a1..c84db7f3ea 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt @@ -59,9 +59,6 @@ internal class StakingBalanceUpdater @AssistedInject constructor( suspend fun partialUpdate() { coroutineScope { listOf( - async { - updateStakeBalance() - }, async { updateNetworkStatuses(delay = 0) }, @@ -78,6 +75,9 @@ internal class StakingBalanceUpdater @AssistedInject constructor( async { updateStakeBalance() }, + async { + updateProcessingActions() + }, ).awaitAll() } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index 293ba68c01..9bc454ab92 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -68,7 +68,7 @@ internal object InitialStakingStatePreview { rewardsCrypto = "100 SOL", rewardBlockType = RewardBlockType.RewardUnavailable, isActionable = true, - balance = persistentListOf( + balances = persistentListOf( BalanceState( groupId = "groupId", title = stringReference("Binance"), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt index 434b40908c..532054c768 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt @@ -2,7 +2,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.common.ui.amountScreen.converters.AmountStateConverter import com.tangem.common.ui.amountScreen.models.AmountParameters -import com.tangem.common.ui.amountScreen.models.MaxEnterAmount +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference @@ -11,7 +11,7 @@ import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import com.tangem.utils.Provider import com.tangem.utils.transformer.Transformer @@ -21,30 +21,36 @@ internal class SetAmountDataTransformer( private val cryptoCurrencyStatusProvider: Provider, private val userWalletProvider: Provider, private val appCurrencyProvider: Provider, - private val maxEnterAmountProvider: Provider, ) : Transformer { private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) - private val amountStateConverter by lazy(LazyThreadSafetyMode.NONE) { - AmountStateConverter( - clickIntents = clickIntents, - cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, - appCurrencyProvider = appCurrencyProvider, - iconStateConverter = iconStateConverter, - maxEnterAmountProvider = maxEnterAmountProvider, - ) - } - override fun transform(prevState: StakingUiState): StakingUiState { val title = if (prevState.actionType == StakingActionCommonType.Exit) { resourceReference(R.string.staking_staked_amount) } else { stringReference(userWalletProvider().name) } + val cryptoBalanceValue = cryptoCurrencyStatusProvider().value + val (amount, fiatAmount) = if (prevState.actionType != StakingActionCommonType.Enter) { + prevState.balanceState?.cryptoAmount to prevState.balanceState?.fiatAmount + } else { + cryptoBalanceValue.amount to cryptoBalanceValue.fiatAmount + } + val maxEnterAmount = EnterAmountBoundary( + amount = amount, + fiatAmount = fiatAmount, + fiatRate = cryptoBalanceValue.fiatRate, + ) return prevState.copy( - amountState = amountStateConverter.convert( + amountState = AmountStateConverter( + clickIntents = clickIntents, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + appCurrencyProvider = appCurrencyProvider, + iconStateConverter = iconStateConverter, + maxEnterAmount = maxEnterAmount, + ).convert( AmountParameters( title = title, value = "", diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index e4cc55d35c..6825aeb4f6 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -4,7 +4,7 @@ import com.tangem.common.extensions.remove import com.tangem.common.ui.amountScreen.converters.AmountStateConverter import com.tangem.common.ui.amountScreen.models.AmountParameters import com.tangem.common.ui.amountScreen.models.AmountState -import com.tangem.common.ui.amountScreen.models.MaxEnterAmount +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.extensions.* @@ -41,21 +41,10 @@ internal class SetInitialDataStateTransformer( private val userWalletProvider: Provider, private val appCurrencyProvider: Provider, private val balancesToShowProvider: Provider>, - private val maxEnterAmountProvider: Provider, ) : Transformer { private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) - private val amountStateConverter by lazy(LazyThreadSafetyMode.NONE) { - AmountStateConverter( - clickIntents = clickIntents, - cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, - appCurrencyProvider = appCurrencyProvider, - iconStateConverter = iconStateConverter, - maxEnterAmountProvider = maxEnterAmountProvider, - ) - } - private val rewardsValidatorStateConverter by lazy(LazyThreadSafetyMode.NONE) { RewardsValidatorStateConverter(cryptoCurrencyStatusProvider, appCurrencyProvider, yield) } @@ -75,7 +64,7 @@ internal class SetInitialDataStateTransformer( title = TextReference.EMPTY, cryptoCurrencyName = cryptoCurrency.name, cryptoCurrencySymbol = cryptoCurrency.symbol, - cryptoCurrencyNetworkId = cryptoCurrency.network.id.value, + cryptoCurrencyBlockchainId = cryptoCurrency.network.id.value, clickIntents = clickIntents, currentStep = StakingStep.InitialInfo, initialInfoState = createInitialInfoState(), @@ -211,7 +200,19 @@ internal class SetInitialDataStateTransformer( } private fun createInitialAmountState(): AmountState { - return amountStateConverter.convert( + val cryptoBalanceValue = cryptoCurrencyStatusProvider().value + val maxEnterAmount = EnterAmountBoundary( + amount = cryptoBalanceValue.amount, + fiatAmount = cryptoBalanceValue.fiatAmount, + fiatRate = cryptoBalanceValue.fiatRate, + ) + return AmountStateConverter( + clickIntents = clickIntents, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + appCurrencyProvider = appCurrencyProvider, + iconStateConverter = iconStateConverter, + maxEnterAmount = maxEnterAmount, + ).convert( AmountParameters( title = stringReference(userWalletProvider().name), value = "", diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt index cd74d47116..41fdc707cf 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt @@ -2,7 +2,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer -import com.tangem.common.ui.amountScreen.models.MaxEnterAmount +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -11,6 +11,7 @@ import com.tangem.utils.transformer.Transformer internal class AmountChangeStateTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val minimumTransactionAmount: EnterAmountBoundary?, private val value: String, private val yield: Yield, ) : Transformer { @@ -20,7 +21,7 @@ internal class AmountChangeStateTransformer( override fun transform(prevState: StakingUiState): StakingUiState { val actionType = prevState.actionType val maxEnterAmount = if (actionType == StakingActionCommonType.Exit) { - MaxEnterAmount( + EnterAmountBoundary( amount = prevState.balanceState?.cryptoAmount, fiatAmount = prevState.balanceState?.fiatAmount, fiatRate = cryptoCurrencyStatus.value.fiatRate, @@ -29,7 +30,12 @@ internal class AmountChangeStateTransformer( maxEnterAmountConverter.convert(cryptoCurrencyStatus) } - val updatedAmountState = AmountFieldChangeTransformer(maxEnterAmount, value).transform(prevState.amountState) + val updatedAmountState = AmountFieldChangeTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + maxEnterAmount = maxEnterAmount, + minimumTransactionAmount = minimumTransactionAmount, + value = value, + ).transform(prevState.amountState) return prevState.copy( amountState = AmountRequirementStateTransformer( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt index 034398c337..db2865b4ec 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt @@ -2,7 +2,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter import com.tangem.common.ui.amountScreen.converters.field.AmountFieldSetMaxAmountTransformer -import com.tangem.common.ui.amountScreen.models.MaxEnterAmount +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -11,6 +11,7 @@ import com.tangem.utils.transformer.Transformer internal class AmountMaxValueStateTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val minimumTransactionAmount: EnterAmountBoundary?, private val actionType: StakingActionCommonType, private val yield: Yield, ) : Transformer { @@ -19,7 +20,7 @@ internal class AmountMaxValueStateTransformer( override fun transform(prevState: StakingUiState): StakingUiState { val maxEnterAmount = if (actionType == StakingActionCommonType.Exit) { - MaxEnterAmount( + EnterAmountBoundary( amount = prevState.balanceState?.cryptoAmount, fiatAmount = prevState.balanceState?.fiatAmount, fiatRate = cryptoCurrencyStatus.value.fiatRate, @@ -28,8 +29,11 @@ internal class AmountMaxValueStateTransformer( maxEnterAmountConverter.convert(cryptoCurrencyStatus) } - val updatedAmountState = AmountFieldSetMaxAmountTransformer(maxEnterAmount) - .transform(prevState.amountState) + val updatedAmountState = AmountFieldSetMaxAmountTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + maxAmount = maxEnterAmount, + minAmount = minimumTransactionAmount, + ).transform(prevState.amountState) return prevState.copy( amountState = AmountRequirementStateTransformer( cryptoCurrencyStatus = cryptoCurrencyStatus, 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 index 91468a21bd..f5e92f66ec 100644 --- 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 @@ -2,18 +2,24 @@ 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.common.ui.amountScreen.models.EnterAmountBoundary 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 minimumTransactionAmount: EnterAmountBoundary?, private val value: ReduceByData, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( - amountState = AmountReduceByTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState), + amountState = AmountReduceByTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + minimumTransactionAmount = minimumTransactionAmount, + value = 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 index b826aae59e..5f7b5e5e57 100644 --- 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 @@ -1,6 +1,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount import com.tangem.common.ui.amountScreen.converters.AmountReduceToTransformer +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer @@ -8,11 +9,16 @@ import java.math.BigDecimal internal class AmountReduceToStateTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val minimumTransactionAmount: EnterAmountBoundary?, private val value: BigDecimal, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( - amountState = AmountReduceToTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState), + amountState = AmountReduceToTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + minimumTransactionAmount = minimumTransactionAmount, + value = 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/AmountRequirementStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt index 199707ada1..cb0b0e31e9 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt @@ -1,10 +1,10 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount +import androidx.annotation.StringRes import androidx.compose.ui.text.input.ImeAction import com.tangem.common.extensions.isZero import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.isNullOrEmpty import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.crypto @@ -26,25 +26,18 @@ internal class AmountRequirementStateTransformer( private val actionType: StakingActionCommonType, ) : Transformer { override fun transform(prevState: AmountState): AmountState { - val amountRequirements = yield.args.enter.args[Yield.Args.ArgType.AMOUNT] - - return if (prevState is AmountState.Data && amountRequirements != null) { + return if (prevState is AmountState.Data) { updateWithError( prevState, actionType, - amountRequirements, ) } else { prevState } } - private fun updateWithError( - amountState: AmountState.Data, - actionType: StakingActionCommonType, - amountRequirements: AddressArgument, - ): AmountState { - val isRequirementError = isRequirementError(amountState, amountRequirements) + private fun updateWithError(amountState: AmountState.Data, actionType: StakingActionCommonType): AmountState { + val requirementError = getRequirementError(amountState) val isIntegerOnlyError = isIntegerOnlyError(amountState, actionType) val cryptoAmount = amountState.amountTextField.cryptoAmount @@ -53,14 +46,7 @@ internal class AmountRequirementStateTransformer( val errorText = when { amountState.amountTextField.isError -> amountState.amountTextField.error - isRequirementError -> resourceReference( - R.string.staking_amount_requirement_error, - wrappedList( - amountRequirements.minimum.format { - crypto(cryptoCurrencyStatus.currency) - }, - ), - ) + requirementError != null -> requirementError isIntegerOnlyError -> when (actionType) { StakingActionCommonType.Enter -> resourceReference( R.string.staking_amount_tron_integer_error, @@ -70,39 +56,43 @@ internal class AmountRequirementStateTransformer( R.string.staking_amount_tron_integer_error_unstaking, wrappedList(value), ) - else -> TODO() + else -> null } - else -> TextReference.EMPTY + else -> null } - val isError = amountState.amountTextField.isError || isRequirementError - return if (!errorText.isNullOrEmpty()) { - amountState.copy( - isPrimaryButtonEnabled = !isError, - amountTextField = amountState.amountTextField.copy( - isError = isError, - isWarning = isIntegerOnlyError, - error = errorText, - keyboardOptions = amountState.amountTextField.keyboardOptions.copy( - imeAction = ImeAction.None, - ), + val isError = amountState.amountTextField.isError || requirementError != null + return amountState.copy( + isPrimaryButtonEnabled = !isError, + amountTextField = amountState.amountTextField.copy( + isError = isError, + isWarning = isIntegerOnlyError, + error = errorText ?: amountState.amountTextField.error, + keyboardOptions = amountState.amountTextField.keyboardOptions.copy( + imeAction = ImeAction.None, ), - ) - } else { - amountState - } + ), + ) } - private fun isRequirementError(prevState: AmountState.Data, amountRequirements: AddressArgument): Boolean { - val amountDecimal = prevState.amountTextField.cryptoAmount.value ?: return false + private fun getRequirementError(prevState: AmountState.Data): TextReference? { + val amountDecimal = prevState.amountTextField.cryptoAmount.value ?: return null val isAlreadyErrorState = prevState.amountTextField.isError - val isAmountRequired = amountRequirements.required val isAmountZero = amountDecimal.isZero() - val isExceedsRequirements = - amountRequirements.maximum?.compareTo(amountDecimal) == -1 || - amountRequirements.minimum?.compareTo(amountDecimal) == 1 - return !isAmountZero && isAmountRequired && isExceedsRequirements && !isAlreadyErrorState + if (isAlreadyErrorState || isAmountZero) return null + + return when (actionType) { + StakingActionCommonType.Enter -> { + val enterRequirements = yield.args.enter.args[Yield.Args.ArgType.AMOUNT] + enterRequirements?.getError(amountDecimal, R.string.staking_amount_requirement_error) + } + StakingActionCommonType.Exit -> { + val exitRequirements = yield.args.exit?.args?.get(Yield.Args.ArgType.AMOUNT) + exitRequirements?.getError(amountDecimal, R.string.staking_unstake_amount_requirement_error) + } + else -> null + } } private fun isIntegerOnlyError(amountState: AmountState.Data, actionType: StakingActionCommonType): Boolean { @@ -116,6 +106,20 @@ internal class AmountRequirementStateTransformer( return isEnterOrExit && isTron && !isIntegerOnly } + private fun AddressArgument.getError(amount: BigDecimal, @StringRes errorTextRes: Int): TextReference? { + val isExceedsRequirements = maximum?.compareTo(amount) == -1 || + minimum?.compareTo(amount) == 1 + + return resourceReference( + errorTextRes, + wrappedList( + minimum.format { + crypto(cryptoCurrencyStatus.currency) + }, + ), + ).takeIf { required && isExceedsRequirements } + } + data class Data( val amountState: AmountState, val actionType: StakingActionCommonType, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt index 2f086ac68c..530e3cc24b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt @@ -107,6 +107,7 @@ internal class AddStakingNotificationsTransformer( notifications = this, prevState = prevState, sendingAmount = sendingAmount, + actionAmount = amountValue, feeValue = feeValue, ) }.toImmutableList() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt index f610a6b17b..1a527dd242 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt @@ -17,6 +17,7 @@ import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.lib.crypto.BlockchainUtils.isCosmos import com.tangem.lib.crypto.BlockchainUtils.isTron import com.tangem.utils.Provider +import com.tangem.utils.extensions.isZero import com.tangem.utils.extensions.orZero import java.math.BigDecimal @@ -26,12 +27,22 @@ internal class StakingInfoNotificationsFactory( private val isSubtractAvailable: Boolean, ) { + /** + * @param notifications current notification to display + * @param prevState current screen state to update + * @param sendingAmount amount being transferred from user account + * @param actionAmount any amount being transferred or used action + * @param feeValue fee amount payed from user account + */ fun addInfoNotifications( notifications: MutableList, prevState: StakingUiState, sendingAmount: BigDecimal, + actionAmount: BigDecimal, feeValue: BigDecimal, ) = with(notifications) { + addStakingLowBalanceNotification(prevState, actionAmount) + when (prevState.actionType) { StakingActionCommonType.Enter -> addEnterInfoNotifications(sendingAmount, feeValue) StakingActionCommonType.Exit -> addExitInfoNotifications() @@ -75,7 +86,9 @@ internal class StakingInfoNotificationsFactory( resourceReference(R.string.staking_restake) to resourceReference(R.string.staking_notification_restake_rewards_text) } - StakingActionType.WITHDRAW -> { + StakingActionType.CLAIM_UNSTAKED, + StakingActionType.WITHDRAW, + -> { resourceReference(R.string.staking_withdraw) to resourceReference(R.string.staking_notification_withdraw_text) } @@ -149,4 +162,21 @@ internal class StakingInfoNotificationsFactory( add(StakingNotification.Info.StakeEntireBalance) } } + + private fun MutableList.addStakingLowBalanceNotification( + prevState: StakingUiState, + actionAmount: BigDecimal, + ) { + if (prevState.actionType != StakingActionCommonType.Exit) return + + val maxAmount = prevState.balanceState?.cryptoAmount ?: return + val exitRequirements = yield.args.exit?.args?.get(Yield.Args.ArgType.AMOUNT) ?: return + + val amountLeft = maxAmount - actionAmount + val isNotEnoughLeft = !amountLeft.isZero() && amountLeft < exitRequirements.minimum.orZero() + + if (exitRequirements.required && isNotEnoughLeft) { + add(StakingNotification.Warning.LowStakedBalance) + } + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt index 558aa4252e..9287657626 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt @@ -16,9 +16,10 @@ import kotlinx.collections.immutable.toPersistentList internal fun StakingActionType?.getPendingActionTitle(): TextReference = when (this) { StakingActionType.CLAIM_REWARDS -> resourceReference(R.string.common_claim_rewards) StakingActionType.RESTAKE_REWARDS -> resourceReference(R.string.staking_restake_rewards) - StakingActionType.WITHDRAW -> resourceReference(R.string.staking_withdraw) + StakingActionType.CLAIM_UNSTAKED, + StakingActionType.WITHDRAW, + -> resourceReference(R.string.staking_withdraw) StakingActionType.RESTAKE -> resourceReference(R.string.staking_restake) - StakingActionType.CLAIM_UNSTAKED -> resourceReference(R.string.staking_claim_unstaked) StakingActionType.UNLOCK_LOCKED -> resourceReference(R.string.staking_unlocked_locked) StakingActionType.STAKE_LOCKED -> resourceReference(R.string.staking_stake_locked) StakingActionType.VOTE -> resourceReference(R.string.staking_vote) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt index ad11c31650..2df8cdd525 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt @@ -32,6 +32,7 @@ import com.tangem.features.staking.impl.presentation.ui.block.StakingFeeBlock import com.tangem.features.staking.impl.presentation.ui.block.ValidatorBlock import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +@Suppress("LongParameterList") @Composable internal fun StakingConfirmationContent( amountState: AmountState, @@ -39,9 +40,10 @@ internal fun StakingConfirmationContent( validatorState: StakingStates.ValidatorState, clickIntents: StakingClickIntents, type: StakingActionCommonType, + isSolana: Boolean, // TODO staking [REDACTED_TASK_KEY] support solana multisize hashes signing ) { if (state !is StakingStates.ConfirmationState.Data) return - val isEnterAction = type == StakingActionCommonType.Enter + val isAmountEditable = type == StakingActionCommonType.Enter || type == StakingActionCommonType.Exit && !isSolana val isTransactionSent = state.innerState == InnerConfirmationStakingState.COMPLETED val isTransactionInProgress = state.notifications.any { it is StakingNotification.Warning.TransactionInProgress } Column( @@ -63,8 +65,8 @@ internal fun StakingConfirmationContent( } AmountBlock( amountState = amountState, - isClickDisabled = !isEnterAction || isTransactionSent || isTransactionInProgress, - isEditingDisabled = !isEnterAction && state.innerState != InnerConfirmationStakingState.COMPLETED, + isClickDisabled = !isAmountEditable || isTransactionSent || isTransactionInProgress, + isEditingDisabled = !isAmountEditable && state.innerState != InnerConfirmationStakingState.COMPLETED, onClick = clickIntents::onPrevClick, ) ValidatorBlock( @@ -89,6 +91,7 @@ private fun Preview_StakingConfirmationContent() { validatorState = ValidatorStatePreviewData.validatorState, clickIntents = StakingClickIntentsStub, type = StakingActionCommonType.Enter, + isSolana = false, ) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index d0d364184d..95038b16d9 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -128,18 +128,20 @@ private fun LazyListScope.activeStakingBlock( clickIntents: StakingClickIntents, isBalanceHidden: Boolean, ) { - val balances = (state.yieldBalance as? InnerYieldBalanceState.Data)?.balance - if (!balances.isNullOrEmpty()) { - item(key = STAKING_REWARD_BLOCK_KEY) { - Column(modifier = Modifier.animateItem()) { - StakingRewardBlock( - yieldBalanceState = state.yieldBalance, - onRewardsClick = clickIntents::openRewardsValidators, - isBalanceHidden = isBalanceHidden, - ) - SpacerH12() - } + val innerYieldBalanceState = state.yieldBalance as? InnerYieldBalanceState.Data ?: return + + item(key = STAKING_REWARD_BLOCK_KEY) { + Column(modifier = Modifier.animateItem()) { + StakingRewardBlock( + yieldBalanceState = state.yieldBalance, + onRewardsClick = clickIntents::openRewardsValidators, + isBalanceHidden = isBalanceHidden, + ) + SpacerH12() } + } + + if (innerYieldBalanceState.balances.isNotEmpty()) { item(ACTIVE_STAKING_BLOCK_KEY) { Text( text = stringResource(id = R.string.staking_your_stakes), @@ -148,7 +150,7 @@ private fun LazyListScope.activeStakingBlock( modifier = Modifier .roundedShapeItemDecoration( currentIndex = 0, - lastIndex = 1 + state.yieldBalance.balance.lastIndex, + lastIndex = 1 + state.yieldBalance.balances.lastIndex, addDefaultPadding = false, ) .fillMaxWidth() @@ -162,10 +164,14 @@ private fun LazyListScope.activeStakingBlock( ) } itemsIndexed( - items = state.yieldBalance.balance, - key = { _, balance -> + items = state.yieldBalance.balances, + key = { index, balance -> // Staked balance does not have unique identifier. - balance.toString() + buildString { + append(balance.hashCode()) + append("_") + append(index) + } }, ) { index, balance -> ActiveStakingBlock( @@ -177,7 +183,7 @@ private fun LazyListScope.activeStakingBlock( .animateItem() .roundedShapeItemDecoration( currentIndex = index + 1, - lastIndex = state.yieldBalance.balance.lastIndex + 1, + lastIndex = state.yieldBalance.balances.lastIndex + 1, addDefaultPadding = false, ), ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index 21be700364..8e6b03d04c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -28,6 +28,7 @@ import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingAc import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingInfoBottomSheetConfig import com.tangem.features.staking.impl.presentation.ui.bottomsheet.StakingActionSelectorBottomSheet import com.tangem.features.staking.impl.presentation.ui.bottomsheet.StakingInfoBottomSheet +import com.tangem.lib.crypto.BlockchainUtils.isSolana import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -170,6 +171,7 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M validatorState = uiState.validatorState, clickIntents = uiState.clickIntents, type = uiState.actionType, + isSolana = isSolana(uiState.cryptoCurrencyBlockchainId), ) StakingStep.RestakeValidator, StakingStep.Validators, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index b35ff9b104..b58a5d7dfb 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -11,7 +11,7 @@ import com.tangem.common.routing.AppRoute import com.tangem.common.routing.bundle.unbundle import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer import com.tangem.common.ui.amountScreen.models.AmountState -import com.tangem.common.ui.amountScreen.models.MaxEnterAmount +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig import com.tangem.common.ui.notifications.NotificationUM @@ -37,6 +37,7 @@ import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.stakekit.* import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.tokens.* import com.tangem.domain.tokens.model.CryptoCurrency @@ -73,6 +74,7 @@ import com.tangem.features.staking.impl.presentation.state.utils.withStubUnstake import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import com.tangem.utils.extensions.isSingleItem +import com.tangem.utils.extensions.orZero import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -93,6 +95,7 @@ internal class StakingViewModel @Inject constructor( private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, + private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val sendTransactionUseCase: SendTransactionUseCase, @@ -140,6 +143,7 @@ internal class StakingViewModel @Inject constructor( private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var processingActions: List = emptyList() private var feeCryptoCurrencyStatus: CryptoCurrencyStatus? = null + private var minimumTransactionAmount: EnterAmountBoundary? = null private var innerRouter: InnerStakingRouter by Delegates.notNull() private var userWallet: UserWallet by Delegates.notNull() @@ -154,14 +158,6 @@ internal class StakingViewModel @Inject constructor( ).getOrElse { emptyList() } } - private val maxEnterAmount: MaxEnterAmount - get() = - MaxEnterAmount( - amount = uiState.value.balanceState?.cryptoAmount, - fiatAmount = uiState.value.balanceState?.fiatAmount, - fiatRate = cryptoCurrencyStatus.value.fiatRate, - ) - private var isInitialInfoAnalyticSent: Boolean = false private val balanceUpdater by lazy(LazyThreadSafetyMode.NONE) { @@ -403,7 +399,14 @@ internal class StakingViewModel @Inject constructor( } override fun onAmountValueChange(value: String) { - stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, value, yield)) + stateController.update( + AmountChangeStateTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + minimumTransactionAmount = minimumTransactionAmount, + value = value, + yield = yield, + ), + ) } override fun onAmountPasteTriggerDismiss() { @@ -415,6 +418,7 @@ internal class StakingViewModel @Inject constructor( stateController.update( AmountMaxValueStateTransformer( cryptoCurrencyStatus = cryptoCurrencyStatus, + minimumTransactionAmount = minimumTransactionAmount, actionType = uiState.value.actionType, yield = yield, ), @@ -658,6 +662,7 @@ internal class StakingViewModel @Inject constructor( ) { AmountReduceByStateTransformer( cryptoCurrencyStatus = cryptoCurrencyStatus, + minimumTransactionAmount = minimumTransactionAmount, value = AmountReduceByTransformer.ReduceByData( reduceAmountBy = reduceAmountBy, reduceAmountByDiff = reduceAmountByDiff, @@ -670,6 +675,7 @@ internal class StakingViewModel @Inject constructor( stateController.update( AmountReduceToStateTransformer( cryptoCurrencyStatus = cryptoCurrencyStatus, + minimumTransactionAmount = minimumTransactionAmount, value = reduceAmountTo, ), ) @@ -833,6 +839,13 @@ internal class StakingViewModel @Inject constructor( feeCryptoCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase(userWalletId, status).getOrNull() + minimumTransactionAmount = + getMinimumTransactionAmountSyncUseCase(userWalletId, status).getOrNull()?.let { + EnterAmountBoundary( + amount = it, + fiatRate = status.value.fiatRate.orZero(), + ) + } cryptoCurrencyStatus = status setupApprovalNeeded() @@ -929,7 +942,6 @@ internal class StakingViewModel @Inject constructor( userWalletProvider = Provider { userWallet }, appCurrencyProvider = Provider { appCurrency }, balancesToShowProvider = Provider { balancesToShow }, - maxEnterAmountProvider = Provider { maxEnterAmount }, ), SetConfirmationStateEmptyTransformer, ) @@ -946,7 +958,7 @@ internal class StakingViewModel @Inject constructor( stateController.updateAll( SetConfirmationStateInitTransformer( isEnter = false, - isExplicitExit = balanceType == BalanceType.STAKED, + isExplicitExit = isExplicitExit(balanceType, pendingAction), balanceState = balanceState, cryptoCurrencyStatus = cryptoCurrencyStatus, stakingApproval = stakingApproval, @@ -963,16 +975,20 @@ internal class StakingViewModel @Inject constructor( cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, userWalletProvider = Provider { userWallet }, appCurrencyProvider = Provider { appCurrency }, - maxEnterAmountProvider = Provider { maxEnterAmount }, ), AmountChangeStateTransformer( cryptoCurrencyStatus = cryptoCurrencyStatus, value = amountValue, + minimumTransactionAmount = minimumTransactionAmount, yield = yield, ), ) } + private fun isExplicitExit(balanceType: BalanceType, pendingAction: PendingAction?): Boolean { + return balanceType == BalanceType.STAKED && pendingAction?.type != StakingActionType.RESTAKE + } + private fun isAssentState(): Boolean { return value.currentStep == StakingStep.Confirmation && (value.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState == diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 436f3cbc1e..4072c1736a 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -5,6 +5,7 @@ import arrow.core.getOrElse import com.tangem.blockchain.common.* import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Blockchain.* import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchainsdk.utils.fromNetworkId @@ -45,7 +46,6 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.firstOrNull import timber.log.Timber import java.math.BigDecimal import java.math.BigInteger @@ -865,8 +865,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( } else { when (blockchain) { // region Blockchains with their own fees - Blockchain.VeChain, - Blockchain.VeChainTestnet, + VeChain, + VeChainTestnet, -> { Fee.VeChain( amount = feeAmount, @@ -874,28 +874,28 @@ internal class SwapInteractorImpl @AssistedInject constructor( gasLimit = fee.gasLimit.toLong(), ) } - Blockchain.Aptos, - Blockchain.AptosTestnet, + Aptos, + AptosTestnet, -> { val gasUnitPrice = fee.feeValue.divide( fee.gasLimit.toBigDecimal(), - Blockchain.Aptos.decimals(), + Aptos.decimals(), RoundingMode.HALF_UP, ) Fee.Aptos( amount = feeAmount, gasUnitPrice = gasUnitPrice - .movePointRight(Blockchain.Aptos.decimals()) + .movePointRight(Aptos.decimals()) .toLong(), gasLimit = fee.gasLimit.toLong(), ) } - Blockchain.Filecoin, + Filecoin, -> { val gasUnitPrice = fee.feeValue.divide( BigDecimal(fee.gasLimit), - Blockchain.Filecoin.decimals(), + Filecoin.decimals(), RoundingMode.HALF_UP, ) val feeParams = requireNotNull(fee.params as? TxFee.Params.Filecoin) @@ -903,14 +903,14 @@ internal class SwapInteractorImpl @AssistedInject constructor( Fee.Filecoin( amount = feeAmount, gasUnitPrice = gasUnitPrice - .movePointRight(Blockchain.Filecoin.decimals()) + .movePointRight(Filecoin.decimals()) .toLong(), gasLimit = fee.gasLimit.toLong(), gasPremium = feeParams.gasPremium, ) } - Blockchain.Sui, - Blockchain.SuiTestnet, + Sui, + SuiTestnet, -> { val feeParams = requireNotNull(fee.params as? TxFee.Params.Sui) @@ -922,133 +922,133 @@ internal class SwapInteractorImpl @AssistedInject constructor( } // endregion // region Blockchains with common fees or EVM-like fees - Blockchain.Unknown, - Blockchain.Arbitrum, - Blockchain.ArbitrumTestnet, - Blockchain.Avalanche, - Blockchain.AvalancheTestnet, - Blockchain.Binance, - Blockchain.BinanceTestnet, - Blockchain.BSC, - Blockchain.BSCTestnet, - Blockchain.Bitcoin, - Blockchain.BitcoinTestnet, - Blockchain.BitcoinCash, - Blockchain.BitcoinCashTestnet, - Blockchain.Cardano, - Blockchain.Cosmos, - Blockchain.CosmosTestnet, - Blockchain.Dogecoin, - Blockchain.Ducatus, - Blockchain.Ethereum, - Blockchain.EthereumTestnet, - Blockchain.EthereumClassic, - Blockchain.EthereumClassicTestnet, - Blockchain.Fantom, - Blockchain.FantomTestnet, - Blockchain.Litecoin, - Blockchain.Near, - Blockchain.NearTestnet, - Blockchain.Polkadot, - Blockchain.PolkadotTestnet, - Blockchain.Kava, - Blockchain.KavaTestnet, - Blockchain.Kusama, - Blockchain.Polygon, - Blockchain.PolygonTestnet, - Blockchain.RSK, - Blockchain.Sei, - Blockchain.SeiTestnet, - Blockchain.Stellar, - Blockchain.StellarTestnet, - Blockchain.Solana, - Blockchain.SolanaTestnet, - Blockchain.Tezos, - Blockchain.Tron, - Blockchain.TronTestnet, - Blockchain.XRP, - Blockchain.Gnosis, - Blockchain.Dash, - Blockchain.Optimism, - Blockchain.OptimismTestnet, - Blockchain.Dischain, - Blockchain.EthereumPow, - Blockchain.EthereumPowTestnet, - Blockchain.Kaspa, - Blockchain.Telos, - Blockchain.TelosTestnet, - Blockchain.TON, - Blockchain.TONTestnet, - Blockchain.Ravencoin, - Blockchain.RavencoinTestnet, - Blockchain.TerraV1, - Blockchain.TerraV2, - Blockchain.Cronos, - Blockchain.AlephZero, - Blockchain.AlephZeroTestnet, - Blockchain.OctaSpace, - Blockchain.OctaSpaceTestnet, - Blockchain.Chia, - Blockchain.ChiaTestnet, - Blockchain.Decimal, - Blockchain.DecimalTestnet, - Blockchain.XDC, - Blockchain.XDCTestnet, - Blockchain.Playa3ull, - Blockchain.Shibarium, - Blockchain.ShibariumTestnet, - Blockchain.Algorand, - Blockchain.AlgorandTestnet, - Blockchain.Hedera, - Blockchain.HederaTestnet, - Blockchain.Aurora, - Blockchain.AuroraTestnet, - Blockchain.Areon, - Blockchain.AreonTestnet, - Blockchain.PulseChain, - Blockchain.PulseChainTestnet, - Blockchain.ZkSyncEra, - Blockchain.ZkSyncEraTestnet, - Blockchain.Nexa, - Blockchain.NexaTestnet, - Blockchain.Moonbeam, - Blockchain.MoonbeamTestnet, - Blockchain.Manta, - Blockchain.MantaTestnet, - Blockchain.PolygonZkEVM, - Blockchain.PolygonZkEVMTestnet, - Blockchain.Radiant, - Blockchain.Base, - Blockchain.BaseTestnet, - Blockchain.Moonriver, - Blockchain.MoonriverTestnet, - Blockchain.Mantle, - Blockchain.MantleTestnet, - Blockchain.Flare, - Blockchain.FlareTestnet, - Blockchain.Taraxa, - Blockchain.TaraxaTestnet, - Blockchain.Koinos, - Blockchain.KoinosTestnet, - Blockchain.Joystream, - Blockchain.Bittensor, - Blockchain.Blast, - Blockchain.BlastTestnet, - Blockchain.Cyber, - Blockchain.CyberTestnet, - Blockchain.InternetComputer, - Blockchain.EnergyWebChain, - Blockchain.EnergyWebChainTestnet, - Blockchain.EnergyWebX, - Blockchain.EnergyWebXTestnet, - Blockchain.Casper, - Blockchain.CasperTestnet, - Blockchain.Core, - Blockchain.CoreTestnet, - Blockchain.Chiliz, - Blockchain.ChilizTestnet, - Blockchain.Xodex, - Blockchain.Canxium, + Unknown, + Arbitrum, + ArbitrumTestnet, + Avalanche, + AvalancheTestnet, + Binance, + BinanceTestnet, + BSC, + BSCTestnet, + Bitcoin, + BitcoinTestnet, + BitcoinCash, + BitcoinCashTestnet, + Cardano, + Cosmos, + CosmosTestnet, + Dogecoin, + Ducatus, + Ethereum, + EthereumTestnet, + EthereumClassic, + EthereumClassicTestnet, + Fantom, + FantomTestnet, + Litecoin, + Near, + NearTestnet, + Polkadot, + PolkadotTestnet, + Kava, + KavaTestnet, + Kusama, + Polygon, + PolygonTestnet, + RSK, + Sei, + SeiTestnet, + Stellar, + StellarTestnet, + Solana, + SolanaTestnet, + Tezos, + Tron, + TronTestnet, + XRP, + Gnosis, + Dash, + Optimism, + OptimismTestnet, + Dischain, + EthereumPow, + EthereumPowTestnet, + Kaspa, + Telos, + TelosTestnet, + TON, + TONTestnet, + Ravencoin, + RavencoinTestnet, + TerraV1, + TerraV2, + Cronos, + AlephZero, + AlephZeroTestnet, + OctaSpace, + OctaSpaceTestnet, + Chia, + ChiaTestnet, + Decimal, + DecimalTestnet, + XDC, + XDCTestnet, + Playa3ull, + Shibarium, + ShibariumTestnet, + Algorand, + AlgorandTestnet, + Hedera, + HederaTestnet, + Aurora, + AuroraTestnet, + Areon, + AreonTestnet, + PulseChain, + PulseChainTestnet, + ZkSyncEra, + ZkSyncEraTestnet, + Nexa, + NexaTestnet, + Moonbeam, + MoonbeamTestnet, + Manta, + MantaTestnet, + PolygonZkEVM, + PolygonZkEVMTestnet, + Radiant, + Base, + BaseTestnet, + Moonriver, + MoonriverTestnet, + Mantle, + MantleTestnet, + Flare, + FlareTestnet, + Taraxa, + TaraxaTestnet, + Koinos, + KoinosTestnet, + Joystream, + Bittensor, + Blast, + BlastTestnet, + Cyber, + CyberTestnet, + InternetComputer, + EnergyWebChain, + EnergyWebChainTestnet, + EnergyWebX, + EnergyWebXTestnet, + Casper, + CasperTestnet, + Core, + CoreTestnet, + Xodex, + Canxium, + Chiliz, + ChilizTestnet, -> Fee.Common(feeAmount) // endregion } @@ -1594,12 +1594,25 @@ internal class SwapInteractorImpl @AssistedInject constructor( amount: BigDecimal, userWallet: UserWallet, cryptoCurrency: CryptoCurrency, - ): Either? { + ): Either { return estimateFeeUseCase( amount = amount, userWallet = userWallet, cryptoCurrency = cryptoCurrency, - ).firstOrNull() + ).mapFeeToForceSingleFeeIfNeeded(cryptoCurrency) + } + + private fun Either.mapFeeToForceSingleFeeIfNeeded( + cryptoCurrency: CryptoCurrency, + ): Either { + return this.map { + val blockchain = Blockchain.fromNetworkId(cryptoCurrency.network.backendId) + if (it is TransactionFee.Choosable && forceSingleFeeBlockchains.contains(blockchain)) { + TransactionFee.Single(normal = it.normal) + } else { + it + } + } } @Suppress("LongParameterList", "LongMethod") @@ -2213,6 +2226,17 @@ internal class SwapInteractorImpl @AssistedInject constructor( private val minDemoFee = "0.0001".toBigDecimal() private val normalDemoFee = "0.0002".toBigDecimal() private val priorityDemoFee = "0.0003".toBigDecimal() + + private val forceSingleFeeBlockchains = listOf( + Bitcoin, BitcoinTestnet, + BitcoinCash, BitcoinCashTestnet, + Litecoin, + Dogecoin, + Dash, + Kaspa, + Ravencoin, RavencoinTestnet, + Ducatus, + ) } @AssistedFactory diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 6f3ae57e47..19a49d15b2 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -176,10 +176,15 @@ internal class StateBuilder( val canSelectReceiveToken = mainTokenId != toToken.id.value if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder - val sendInput = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable).copy( - isError = false, - header = TextReference.Res(R.string.swapping_from_title), - ) + val sendInputType = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable) + val sendInput = if (sendInputType.isError) { + sendInputType + } else { + sendInputType.copy( + isError = false, + header = TextReference.Res(R.string.swapping_from_title), + ) + } return uiStateHolder.copy( sendCardData = SwapCardState.SwapCardData( type = sendInput, @@ -256,10 +261,16 @@ internal class StateBuilder( } else { TextReference.Res(R.string.swapping_from_title) } - val sendInput = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable).copy( - isError = isInsufficientFunds, - header = insufficientFundsHeader, - ) + val sendCardType = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable) + val sendInput = if (sendCardType.isError && !isInsufficientFunds) { + // if any error in inputField and funds enough -> show that error else show fund is not enough error + sendCardType + } else { + sendCardType.copy( + isError = isInsufficientFunds, + header = insufficientFundsHeader, + ) + } return uiStateHolder.copy( sendCardData = SwapCardState.SwapCardData( type = sendInput, @@ -867,14 +878,36 @@ internal class StateBuilder( ) } - fun updateSwapAmount(uiState: SwapStateHolder, amount: String): SwapStateHolder { + fun updateSwapAmount( + uiState: SwapStateHolder, + amountFormatted: String, + amountRaw: String, + fromToken: CryptoCurrency, + minTxAmount: BigDecimal?, + ): SwapStateHolder { if (uiState.sendCardData !is SwapCardState.SwapCardData) return uiState + val amountToSend = amountRaw.toBigDecimalOrNull() + val sendInput = if (minTxAmount != null && amountToSend != null && amountToSend < minTxAmount) { + val minAmountFormatted = minTxAmount.format { + crypto(cryptoCurrency = fromToken, ignoreSymbolPosition = true) + } + (uiState.sendCardData.type as? TransactionCardType.Inputtable)?.copy( + isError = true, + header = resourceReference(R.string.transfer_min_amount_error, wrappedList(minAmountFormatted)), + ) ?: uiState.sendCardData.type + } else { + (uiState.sendCardData.type as? TransactionCardType.Inputtable)?.copy( + isError = false, + header = TextReference.Res(R.string.swapping_from_title), + ) ?: uiState.sendCardData.type + } return uiState.copy( sendCardData = uiState.sendCardData.copy( amountTextFieldValue = TextFieldValue( - text = amount, - selection = TextRange(amount.length), + text = amountFormatted, + selection = TextRange(amountFormatted.length), ), + type = sendInput, ), ) } @@ -1098,10 +1131,20 @@ internal class StateBuilder( provider: SwapProvider, onDismiss: () -> Unit, ): SwapStateHolder { + val slippage = provider.slippage?.let { "${it.parseBigDecimal(1)}$PERCENT" } val combinedMessage = buildList { when (provider.type) { ExchangeProviderType.CEX -> { - add(resourceReference(R.string.swapping_alert_cex_description, wrappedList(token))) + if (slippage != null) { + add( + resourceReference( + id = R.string.swapping_alert_cex_description_with_slippage, + formatArgs = wrappedList(token, slippage), + ), + ) + } else { + add(resourceReference(R.string.swapping_alert_cex_description, wrappedList(token))) + } } ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE, @@ -1110,18 +1153,18 @@ internal class StateBuilder( add(resourceReference(R.string.swapping_high_price_impact_description)) add(stringReference("\n\n")) } - add(resourceReference(R.string.swapping_alert_dex_description)) + if (slippage != null) { + add( + resourceReference( + id = R.string.swapping_alert_dex_description_with_slippage, + formatArgs = wrappedList(token, slippage), + ), + ) + } else { + add(resourceReference(R.string.swapping_alert_dex_description, wrappedList(token))) + } } } - provider.slippage?.let { slippage -> - add(stringReference("\n\n")) - add( - resourceReference( - R.string.swapping_alert_slippage_description, - wrappedList("${slippage.parseBigDecimal(1)}$PERCENT"), - ), - ) - } } return uiState.copy( event = triggeredEvent( diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index ed003efb7c..3aa4b23a82 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -26,6 +26,7 @@ import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -84,6 +85,7 @@ internal class SwapViewModel @Inject constructor( private val getCardInfoUseCase: GetCardInfoUseCase, private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase, private val walletFeatureToggles: WalletFeatureToggles, swapInteractorFactory: SwapInteractor.Factory, private val savedStateHandle: SavedStateHandle, @@ -866,60 +868,78 @@ internal class SwapViewModel @Inject constructor( } private fun onChangeCardsClicked() { - val newFromToken = dataState.toCryptoCurrency - val newToToken = dataState.fromCryptoCurrency + viewModelScope.launch { + val newFromToken = dataState.toCryptoCurrency + val newToToken = dataState.fromCryptoCurrency - if (newFromToken != null && newToToken != null) { - isAmountChangedByUser = true + if (newFromToken != null && newToToken != null) { + isAmountChangedByUser = true - dataState = dataState.copy( - fromCryptoCurrency = newFromToken, - toCryptoCurrency = newToToken, - ) - isOrderReversed = !isOrderReversed - dataState.tokensDataState?.let { - updateTokensState(it) + dataState = dataState.copy( + fromCryptoCurrency = newFromToken, + toCryptoCurrency = newToToken, + ) + isOrderReversed = !isOrderReversed + dataState.tokensDataState?.let { + updateTokensState(it) + } + + val minTxAmount = getMinimumTransactionAmountSyncUseCase( + userWalletId, + newFromToken, + ).getOrNull() + val decimals = newFromToken.currency.decimals + lastAmount.value = cutAmountWithDecimals(decimals, lastAmount.value) + uiState = stateBuilder.updateSwapAmount( + uiState = uiState, + amountFormatted = inputNumberFormatter.formatWithThousands(lastAmount.value, decimals), + amountRaw = lastAmount.value, + fromToken = newFromToken.currency, + minTxAmount = minTxAmount, + ) + startLoadingQuotes( + fromToken = newFromToken, + toToken = newToToken, + amount = lastAmount.value, + toProvidersList = findSwapProviders(newFromToken, newToToken), + ) } - - val decimals = newFromToken.currency.decimals - lastAmount.value = cutAmountWithDecimals(decimals, lastAmount.value) - uiState = stateBuilder.updateSwapAmount( - uiState, - inputNumberFormatter.formatWithThousands(lastAmount.value, decimals), - ) - startLoadingQuotes( - fromToken = newFromToken, - toToken = newToToken, - amount = lastAmount.value, - toProvidersList = findSwapProviders(newFromToken, newToToken), - ) } } private fun onAmountChanged(value: String) { - val fromToken = dataState.fromCryptoCurrency - val toToken = dataState.toCryptoCurrency - if (fromToken != null) { - val decimals = fromToken.currency.decimals - val cutValue = cutAmountWithDecimals(decimals, value) - lastAmount.value = cutValue - uiState = stateBuilder.updateSwapAmount( - uiState = uiState, - amount = inputNumberFormatter.formatWithThousands(cutValue, decimals), - ) + viewModelScope.launch { + val fromToken = dataState.fromCryptoCurrency + val toToken = dataState.toCryptoCurrency + if (fromToken != null) { + val decimals = fromToken.currency.decimals + val cutValue = cutAmountWithDecimals(decimals, value) + val minTxAmount = getMinimumTransactionAmountSyncUseCase( + userWalletId, + fromToken, + ).getOrNull() + lastAmount.value = cutValue + uiState = stateBuilder.updateSwapAmount( + uiState = uiState, + amountFormatted = inputNumberFormatter.formatWithThousands(cutValue, decimals), + amountRaw = lastAmount.value, + fromToken = fromToken.currency, + minTxAmount = minTxAmount, + ) - if (toToken != null) { - if (toToken.value.amount != null) { - isAmountChangedByUser = true - } + if (toToken != null) { + if (toToken.value.amount != null) { + isAmountChangedByUser = true + } - amountDebouncer.debounce(viewModelScope, DEBOUNCE_AMOUNT_DELAY) { - startLoadingQuotes( - fromToken = fromToken, - toToken = toToken, - amount = lastAmount.value, - toProvidersList = findSwapProviders(fromToken, toToken), - ) + amountDebouncer.debounce(viewModelScope, DEBOUNCE_AMOUNT_DELAY) { + startLoadingQuotes( + fromToken = fromToken, + toToken = toToken, + amount = lastAmount.value, + toProvidersList = findSwapProviders(fromToken, toToken), + ) + } } } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index cc7b3c4d38..a8bf99a969 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -14,7 +14,6 @@ import com.tangem.core.ui.format.bigdecimal.* import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.stakekit.BalanceItem import com.tangem.domain.staking.model.stakekit.RewardBlockType import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.error.CurrencyStatusError @@ -40,7 +39,6 @@ internal class TokenDetailsLoadedBalanceConverter( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, private val stakingEntryInfoProvider: Provider, - private val pendingBalancesProvider: Provider>, private val stakingAvailabilityProvider: Provider, private val symbol: String, private val decimals: Int, @@ -141,53 +139,51 @@ internal class TokenDetailsLoadedBalanceConverter( } private fun getYieldBalance(status: CryptoCurrencyStatus, state: TokenDetailsState): StakingBlockUM? { + return when (stakingAvailabilityProvider.invoke()) { + StakingAvailability.TemporaryUnavailable -> StakingBlockUM.TemporaryUnavailable + StakingAvailability.Unavailable -> null + is StakingAvailability.Available -> getStakingInfoBlock(status, state) + } + } + + private fun getStakingInfoBlock(status: CryptoCurrencyStatus, state: TokenDetailsState): StakingBlockUM? { val yieldBalance = status.value.yieldBalance as? YieldBalance.Data + val stakingCryptoAmount = yieldBalance?.getTotalStakingBalance() + val pendingBalances = yieldBalance?.balance?.items ?: emptyList() val stakingEntryInfo = stakingEntryInfoProvider.invoke() - val stakingAvailability = stakingAvailabilityProvider.invoke() val iconState = state.tokenInfoBlockState.iconState - val pendingBalances = pendingBalancesProvider.invoke() - val fiatRate = status.value.fiatRate return when { - stakingAvailability == StakingAvailability.TemporaryUnavailable -> { - StakingBlockUM.TemporaryUnavailable - } - stakingAvailability == StakingAvailability.Unavailable -> { - null - } - stakingCryptoAmount.isNullOrZero() && stakingEntryInfo != null && pendingBalances.isEmpty() -> { - getStakeAvailableState(stakingEntryInfo, iconState) - } - stakingCryptoAmount.isNullOrZero() && stakingEntryInfo != null && pendingBalances.isNotEmpty() -> { - val pendingBalancesCryptoAmount = pendingBalances.sumOf { it.amount } - - val stakingFiatAmount = fiatRate?.multiply(pendingBalancesCryptoAmount) - getStakedState( - status = status, - stakingCryptoAmount = pendingBalancesCryptoAmount, - stakingFiatAmount = stakingFiatAmount, - stakingRewardAmount = null, - ) + stakingCryptoAmount.isNullOrZero() && stakingEntryInfo != null -> { + if (pendingBalances.isEmpty()) { + getStakeAvailableState(stakingEntryInfo, iconState) + } else { + getStakedBlockWithFiatAmount(status, pendingBalances.sumOf { it.amount }, null) + } } stakingCryptoAmount.isNullOrZero() && stakingEntryInfo == null -> { null } - else -> { - val stakingRewardAmount = yieldBalance?.getRewardStakingBalance()?.let { fiatRate?.multiply(it) } - val stakingFiatAmount = stakingCryptoAmount?.let { fiatRate?.multiply(it) } - - getStakedState( - status = status, - stakingCryptoAmount = stakingCryptoAmount, - stakingFiatAmount = stakingFiatAmount, - stakingRewardAmount = stakingRewardAmount, - ) - } + else -> getStakedBlockWithFiatAmount(status, stakingCryptoAmount, yieldBalance?.getRewardStakingBalance()) } } + private fun getStakedBlockWithFiatAmount( + status: CryptoCurrencyStatus, + stakingAmount: BigDecimal?, + rewardAmount: BigDecimal?, + ): StakingBlockUM.Staked { + val fiatRate = status.value.fiatRate + return getStakedState( + status = status, + stakingCryptoAmount = stakingAmount, + stakingFiatAmount = stakingAmount?.let { fiatRate?.multiply(it) }, + stakingRewardAmount = rewardAmount?.let { fiatRate?.multiply(it) }, + ) + } + private fun getMarketPriceState( status: CryptoCurrencyStatus.Value, currencySymbol: String, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 0f92424ba2..98850f08f2 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -10,9 +10,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.card.NetworkHasDerivationUseCase -import com.tangem.domain.staking.GetStakingAvailabilityUseCase import com.tangem.domain.staking.GetStakingIntegrationIdUseCase -import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId @@ -32,7 +30,6 @@ internal class TokenDetailsSkeletonStateConverter( private val clickIntents: TokenDetailsClickIntents, private val networkHasDerivationUseCase: NetworkHasDerivationUseCase, private val getStakingIntegrationIdUseCase: GetStakingIntegrationIdUseCase, - private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val userWalletId: UserWalletId, ) : Converter { @@ -42,8 +39,6 @@ internal class TokenDetailsSkeletonStateConverter( override fun convert(value: CryptoCurrency): TokenDetailsState { val iconState = iconStateConverter.convert(value) val isSupportedInMobileApp = getStakingIntegrationIdUseCase(value.id).isNullOrBlank().not() - val stakingAvailability = getStakingAvailabilityUseCase(userWalletId, value) - .getOrElse { StakingAvailability.Unavailable } return TokenDetailsState( topAppBarConfig = TokenDetailsTopAppBarConfig( @@ -68,11 +63,7 @@ internal class TokenDetailsSkeletonStateConverter( selectedBalanceType = BalanceType.ALL, ), marketPriceBlockState = MarketPriceBlockState.Loading(value.symbol), - stakingBlocksState = if (stakingAvailability == StakingAvailability.TemporaryUnavailable) { - StakingBlockUM.TemporaryUnavailable - } else { - StakingBlockUM.Loading(iconState).takeIf { isSupportedInMobileApp } - }, + stakingBlocksState = StakingBlockUM.Loading(iconState).takeIf { isSupportedInMobileApp }, notifications = persistentListOf(), pendingTxs = persistentListOf(), swapTxs = persistentListOf(), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 626a072c3c..23aa2143f1 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -17,11 +17,9 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.NetworkHasDerivationUseCase import com.tangem.domain.common.CardTypesResolver -import com.tangem.domain.staking.GetStakingAvailabilityUseCase import com.tangem.domain.staking.GetStakingIntegrationIdUseCase import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.stakekit.BalanceItem import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning @@ -53,13 +51,11 @@ internal class TokenDetailsStateFactory( private val stakingEntryInfoProvider: Provider, private val stakingAvailabilityProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, - private val pendingBalancesProvider: Provider>, private val clickIntents: TokenDetailsClickIntents, private val networkHasDerivationUseCase: NetworkHasDerivationUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val userWalletId: UserWalletId, getStakingIntegrationIdUseCase: GetStakingIntegrationIdUseCase, - getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase, symbol: String, decimals: Int, ) { @@ -69,7 +65,6 @@ internal class TokenDetailsStateFactory( clickIntents = clickIntents, networkHasDerivationUseCase = networkHasDerivationUseCase, getStakingIntegrationIdUseCase = getStakingIntegrationIdUseCase, - getStakingAvailabilityUseCase = getStakingAvailabilityUseCase, getUserWalletUseCase = getUserWalletUseCase, userWalletId = userWalletId, ) @@ -85,7 +80,6 @@ internal class TokenDetailsStateFactory( appCurrencyProvider = appCurrencyProvider, stakingEntryInfoProvider = stakingEntryInfoProvider, stakingAvailabilityProvider = stakingAvailabilityProvider, - pendingBalancesProvider = pendingBalancesProvider, symbol = symbol, decimals = decimals, clickIntents = clickIntents, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt index 4756f994ca..360126d91d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt @@ -50,13 +50,14 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( } else { when (type) { is TransactionType.Approve -> R.drawable.ic_doc_24 - is TransactionType.TronStakingTransactionType.Stake, - is TransactionType.TronStakingTransactionType.Vote, + is TransactionType.Staking.Stake, + is TransactionType.Staking.Vote, + is TransactionType.Staking.Restake, -> R.drawable.ic_transaction_history_staking_24 - is TransactionType.TronStakingTransactionType.ClaimRewards, + is TransactionType.Staking.ClaimRewards, -> R.drawable.ic_transaction_history_claim_rewards_24 - is TransactionType.TronStakingTransactionType.Unstake, - is TransactionType.TronStakingTransactionType.Withdraw, + is TransactionType.Staking.Unstake, + is TransactionType.Staking.Withdraw, -> R.drawable.ic_transaction_history_unstaking_24 is TransactionType.Operation, is TransactionType.Swap, @@ -72,11 +73,12 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( is TransactionType.Swap -> resourceReference(R.string.common_swap) is TransactionType.Transfer -> resourceReference(R.string.common_transfer) is TransactionType.UnknownOperation -> resourceReference(R.string.transaction_history_operation) - is TransactionType.TronStakingTransactionType.Stake -> resourceReference(R.string.common_stake) - is TransactionType.TronStakingTransactionType.Unstake -> resourceReference(R.string.common_unstake) - is TransactionType.TronStakingTransactionType.Vote -> resourceReference(R.string.staking_vote) - is TransactionType.TronStakingTransactionType.ClaimRewards -> resourceReference(R.string.common_claim_rewards) - is TransactionType.TronStakingTransactionType.Withdraw -> resourceReference(R.string.staking_withdraw) + is TransactionType.Staking.Stake -> resourceReference(R.string.common_stake) + is TransactionType.Staking.Unstake -> resourceReference(R.string.common_unstake) + is TransactionType.Staking.Vote -> resourceReference(R.string.staking_vote) + is TransactionType.Staking.ClaimRewards -> resourceReference(R.string.common_claim_rewards) + is TransactionType.Staking.Withdraw -> resourceReference(R.string.staking_withdraw) + is TransactionType.Staking.Restake -> resourceReference(R.string.staking_restake) } private fun TxHistoryItem.extractSubtitle(): TextReference = @@ -119,9 +121,9 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( } private fun TxHistoryItem.getAmount(): String { - if (type is TransactionType.TronStakingTransactionType.Vote || - type == TransactionType.TronStakingTransactionType.ClaimRewards || - type == TransactionType.TronStakingTransactionType.Withdraw + if (type is TransactionType.Staking.Vote || + type == TransactionType.Staking.ClaimRewards || + type == TransactionType.Staking.Withdraw ) { return "" } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index c07f123af4..a83d17ed6f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -38,7 +38,6 @@ import com.tangem.domain.staking.GetStakingIntegrationIdUseCase import com.tangem.domain.staking.GetYieldUseCase import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.stakekit.BalanceItem import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.legacy.TradeCryptoAction.TransactionInfo @@ -147,7 +146,6 @@ internal class TokenDetailsViewModel @Inject constructor( private val warningsJobHolder = JobHolder() private val swapTxJobHolder = JobHolder() private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() - private val stakingPendingBalances: List = emptyList() // TODO staking private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null private var stakingEntryInfo: StakingEntryInfo? = null @@ -160,13 +158,11 @@ internal class TokenDetailsViewModel @Inject constructor( stakingEntryInfoProvider = Provider { stakingEntryInfo }, stakingAvailabilityProvider = Provider { stakingAvailability }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, - pendingBalancesProvider = Provider { stakingPendingBalances }, clickIntents = this, networkHasDerivationUseCase = networkHasDerivationUseCase, getUserWalletUseCase = getUserWalletUseCase, userWalletId = userWalletId, getStakingIntegrationIdUseCase = getStakingIntegrationIdUseCase, - getStakingAvailabilityUseCase = getStakingAvailabilityUseCase, symbol = cryptoCurrency.symbol, decimals = cryptoCurrency.decimals, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt index 461f74999f..a85ca99eab 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt @@ -49,13 +49,14 @@ internal class TxHistoryItemStateConverter( } else { when (type) { is TransactionType.Approve -> R.drawable.ic_doc_24 - is TransactionType.TronStakingTransactionType.Stake, - is TransactionType.TronStakingTransactionType.Vote, + is TransactionType.Staking.Stake, + is TransactionType.Staking.Vote, + is TransactionType.Staking.Restake, -> R.drawable.ic_transaction_history_staking_24 - is TransactionType.TronStakingTransactionType.ClaimRewards, + is TransactionType.Staking.ClaimRewards, -> R.drawable.ic_transaction_history_claim_rewards_24 - is TransactionType.TronStakingTransactionType.Unstake, - is TransactionType.TronStakingTransactionType.Withdraw, + is TransactionType.Staking.Unstake, + is TransactionType.Staking.Withdraw, -> R.drawable.ic_transaction_history_unstaking_24 is TransactionType.Operation, is TransactionType.Swap, @@ -70,11 +71,12 @@ internal class TxHistoryItemStateConverter( is TransactionType.Operation -> stringReference(type.name) is TransactionType.Swap -> resourceReference(R.string.common_swap) is TransactionType.Transfer -> resourceReference(R.string.common_transfer) - is TransactionType.TronStakingTransactionType.Stake -> resourceReference(R.string.common_stake) - is TransactionType.TronStakingTransactionType.Unstake -> resourceReference(R.string.common_unstake) - is TransactionType.TronStakingTransactionType.Vote -> resourceReference(R.string.staking_vote) - is TransactionType.TronStakingTransactionType.ClaimRewards -> resourceReference(R.string.common_claim_rewards) - is TransactionType.TronStakingTransactionType.Withdraw -> { resourceReference(R.string.staking_withdraw) } + is TransactionType.Staking.Stake -> resourceReference(R.string.common_stake) + is TransactionType.Staking.Unstake -> resourceReference(R.string.common_unstake) + is TransactionType.Staking.Vote -> resourceReference(R.string.staking_vote) + is TransactionType.Staking.ClaimRewards -> resourceReference(R.string.common_claim_rewards) + is TransactionType.Staking.Withdraw -> resourceReference(R.string.staking_withdraw) + is TransactionType.Staking.Restake -> resourceReference(R.string.staking_restake) is TransactionType.UnknownOperation -> resourceReference(R.string.transaction_history_operation) } @@ -119,9 +121,9 @@ internal class TxHistoryItemStateConverter( } private fun TxHistoryItem.getAmount(): String { - if (type is TransactionType.TronStakingTransactionType.Vote || - type == TransactionType.TronStakingTransactionType.ClaimRewards || - type == TransactionType.TronStakingTransactionType.Withdraw + if (type is TransactionType.Staking.Vote || + type == TransactionType.Staking.ClaimRewards || + type == TransactionType.Staking.Withdraw ) { return "" } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 676e74e6da..2617c1d2e8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -179,7 +179,11 @@ private fun WalletContent( contentPadding = contentPadding, horizontalAlignment = Alignment.CenterHorizontally, ) { - item(key = "WalletsList" + state.selectedWalletIndex, contentType = "WalletsList") { + item( + // !!! Type of the key should be saveable via Bundle on Android !!! + key = state.wallets.map { it.walletCardState.id.stringValue }, + contentType = state.wallets.map { it.walletCardState.id }, + ) { WalletsList( lazyListState = walletsListState, wallets = state.wallets.map(WalletState::walletCardState).toImmutableList(), diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 5d31f72ead..2e3c74646e 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -69,8 +69,8 @@ zxingQrCode = "3.5.1" mviCore = "1.3.1" kotlinSerialization = "1.4.1" arrow = "1.2.3" -walletConnectCore = "1.18.0" -walletConnectWeb3 = "1.11.0" +walletConnectCore = "1.35.2" +walletConnectWeb3 = "1.35.2" prettyLogger = "2.2.0" okHttp-prettyLogging = "3.1.0" chucker = "4.0.0" @@ -78,7 +78,6 @@ mlKit-barcodeScanning = "17.2.0" androidXCamera = "1.3.0" listenableFuture = "1.0" swipeRefreshLayout = "1.1.0" -spr-client = "3.6.2" web3j = "4.10.1" leakcanary = "2.13" decompose = "2.2.2" @@ -88,9 +87,9 @@ markdownComposeView = "0.5.4" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-860" +tangemBlockchainSdk = "develop-863" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-397" +tangemCardSdk = "develop-400" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem17" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index a9830d7036..46c633a3c9 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -416,4 +416,5 @@ private val excludedBlockchains = listOf( Blockchain.Unknown, Blockchain.Nexa, Blockchain.NexaTestnet, + Blockchain.Xodex, ) \ No newline at end of file