Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-14 11:21:10 +03:00
commit bec9e17f51
93 changed files with 1335 additions and 605 deletions

@ -1 +1 @@
Subproject commit 544358e4640ae25402dd4186e0f244f9509c8906
Subproject commit adbdabe422b0513640d1750f276298683d342f61

View file

@ -268,6 +268,16 @@ internal object TokensDomainModule {
)
}
@Provides
@Singleton
fun provideGetMinimumTransactionAmountSyncUseCase(
currencyChecksRepository: CurrencyChecksRepository,
): GetMinimumTransactionAmountSyncUseCase {
return GetMinimumTransactionAmountSyncUseCase(
currencyChecksRepository = currencyChecksRepository,
)
}
@Provides
@Singleton
fun provideIsCryptoCurrencyCoinCouldHideUseCase(

View file

@ -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<NetworkNamespace, List<Account>>) {
this.userNamespaces = userNamespaces
val sessionProposal: Wallet.Model.SessionProposal = requireNotNull(this.sessionProposal)
val userChains = userNamespaces.flatMap { namespace ->

View file

@ -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)

View file

@ -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<AmountState> {
@ -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,
),
),

View file

@ -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<AmountState> {
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,
),
),

View file

@ -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<AppCurrency>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val maxEnterAmountProvider: Provider<MaxEnterAmount>,
private val maxEnterAmount: EnterAmountBoundary,
private val iconStateConverter: CryptoCurrencyToIconStateConverter,
) : Converter<AmountParameters, AmountState> {
@ -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()

View file

@ -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<CryptoCurrencyStatus, MaxEnterAmount> {
class MaxEnterAmountConverter : Converter<CryptoCurrencyStatus, EnterAmountBoundary> {
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,

View file

@ -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<AmountState> {
@ -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,
),
),

View file

@ -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<AmountState> {
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,
),
),

View file

@ -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,
)
}

View file

@ -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,
)

View file

@ -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(),

View file

@ -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

View file

@ -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,

View file

@ -75,6 +75,20 @@ object NotificationsFactory {
}
}
fun MutableList<NotificationUM>.addMinimumAmountErrorNotification(
minimumSendAmount: BigDecimal?,
sendingAmount: BigDecimal,
cryptoCurrency: CryptoCurrency,
) {
if (minimumSendAmount != null && minimumSendAmount > sendingAmount) {
add(
NotificationUM.Error.MinimumSendAmountError(
amount = minimumSendAmount.format { crypto(cryptoCurrency) },
),
)
}
}
fun MutableList<NotificationUM>.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) },
),
)
}

View file

@ -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(

View file

@ -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 {

View file

@ -17,7 +17,7 @@ internal object StakingStoreModule {
@Provides
@Singleton
fun provideStakingTokensStore(): StakingYieldsStore {
return DefaultStakingYieldsStore()
return DefaultStakingYieldsStore(dataStore = RuntimeDataStore())
}
@Provides

View file

@ -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<List<YieldDTO>>,
) : StakingYieldsStore {
private var yields = listOf<YieldDTO>()
private val mutex = Mutex()
override fun get(): List<YieldDTO> {
return yields
override fun get(): Flow<List<YieldDTO>> {
return dataStore.get(KEY)
}
override fun store(items: List<YieldDTO>) {
yields = items
override suspend fun getSync(): List<YieldDTO> {
return dataStore.getSyncOrNull(KEY) ?: emptyList()
}
override suspend fun store(items: List<YieldDTO>) {
mutex.withLock {
dataStore.store(KEY, items)
}
}
companion object {
private const val KEY = "DefaultStakingYieldsStore"
}
}

View file

@ -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<YieldDTO>
fun get(): Flow<List<YieldDTO>>
fun store(items: List<YieldDTO>)
suspend fun getSync(): List<YieldDTO>
suspend fun store(items: List<YieldDTO>)
}

View file

@ -1,6 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="action_buttons_buy_empty_search_message">Token nicht in Deinem Portfolio gefunden? Überprüfe die Märkte, um es zu finden und zum Kauf hinzuzufügen</string>
<string name="action_buttons_sell_empty_search_message">Token nicht in Deinem Portfolio gefunden? Überprüfe die Märkte, um es zu finden und zum Verkauf hinzuzufügen</string>
<string name="action_buttons_swap_choose_token">Wähle den Token</string>
<string name="action_buttons_swap_empty_search_message">Token nicht in Deinem Portfolio gefunden? Überprüfe die Märkte, um es für Swaps zu finden und hinzuzufügen</string>
<string name="action_buttons_swap_no_available_pair_notification_message">Es gibt keine verfügbaren Token, die mit dem ausgewählten Token getauscht werden können. Bitte wähle einen anderen.</string>
<string name="action_buttons_swap_no_available_pair_notification_title">Kein verfügbares Paar</string>
<string name="action_buttons_you_want_to_receive">Du möchtest erhalten</string>
<string name="action_buttons_you_want_to_swap">Du möchtest tauschen</string>
<string name="add_custom_token_choose_network">Netzwerk wählen</string>
@ -257,7 +262,7 @@
<string name="express_exchange_status_canceled">Abgebrochen</string>
<string name="express_exchange_status_confirmed">Bestätigt</string>
<string name="express_exchange_status_confirming">Bestätigen</string>
<string name="express_exchange_status_confirming_active">wird bestätigt...</string>
<string name="express_exchange_status_confirming_active">Wird bestätigt...</string>
<string name="express_exchange_status_exchanged">Getauscht</string>
<string name="express_exchange_status_exchanging">Tauschen läuft</string>
<string name="express_exchange_status_exchanging_active">Austauschen...</string>
@ -448,6 +453,7 @@
<string name="markets_token_details_price_performance">Preisleistung</string>
<string name="markets_token_details_repository">Aufbewahrungsort</string>
<string name="markets_token_details_security_score">Sicherheitsbewertung</string>
<string name="markets_token_details_security_score_description">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.</string>
<string name="markets_token_details_social">Soziales</string>
<string name="markets_token_details_total_supply">Gesamtangebot</string>
<string name="markets_token_details_total_supply_description">Die maximale Anzahl von Coins oder Tokens, die jemals für eine bestimmte Kryptowährung existieren können</string>
@ -554,10 +560,21 @@
<string name="onboarding_wallet_info_title_fourth">Zugangscode wiederherstellen</string>
<string name="onboarding_wallet_info_title_second">Identische Karten oder Ring</string>
<string name="onboarding_wallet_info_title_third">Zugangscode</string>
<string name="onramp_country_search">Suche nach Land</string>
<string name="onramp_country_unavailable">Nicht verfügbar</string>
<string name="onramp_currency_search">Suche nach Währung</string>
<string name="onramp_max_amount_restriction">Der Kaufbetrag sollte nicht höher sein als %s</string>
<string name="onramp_min_amount_restriction">Der zu kaufende Betrag muss mindestens %s betragen</string>
<string name="onramp_no_available_providers">Keine verfügbaren Anbieter für diese Währung</string>
<string name="onramp_pay_with">Bezahlen mit</string>
<string name="onramp_redirecting_to_provider_subtitle">Du kannst Deine Transaktion beim Drittanbieter %s abschließen.</string>
<string name="onramp_redirecting_to_provider_title">Umleitung auf %s...</string>
<string name="onramp_residency_bottomsheet_country_not_supported">Unsere Dienstleistungen sind in diesem Land nicht verfügbar</string>
<string name="onramp_residency_bottomsheet_country_subtitle">Änder oder bestätige bitte</string>
<string name="onramp_residency_bottomsheet_title">Dein Wohnsitz wurde identifiziert als</string>
<string name="onramp_settings_residence">Residenz</string>
<string name="onramp_settings_residence_description">Bitte wähle das richtige Land aus, um korrekte Zahlungsoptionen und Dienstleistungen zu gewährleisten.</string>
<string name="onramp_settings_title">Einstellungen</string>
<string name="onramp_via">Über</string>
<string name="organize_tokens_group">Gruppe erstellen</string>
<string name="organize_tokens_sort_by_balance">Nach Guthaben</string>
@ -661,7 +678,7 @@
<string name="send_notification_high_fee_text">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.</string>
<string name="send_notification_high_fee_title">Die Gebühr ist höher</string>
<string name="send_notification_invalid_amount_text">Die enthaltene Kommission übersteigt den Überweisungsbetrag, was zu einem negativen Wert führt</string>
<string name="send_notification_invalid_amount_title">ungültige Menge</string>
<string name="send_notification_invalid_amount_title">Ungültige Menge</string>
<string name="send_notification_invalid_minimum_amount_text">Der Mindestbetrag für den Versand beträgt %1$s. Bitte stell sicher, dass der Restbetrag nach dem Versand nicht unter %2$s liegt.</string>
<string name="send_notification_invalid_reserve_amount_text">Das Zielkonto wurde nicht erstellt. Bitte änder den zu sendenden Betrag.</string>
<string name="send_notification_invalid_reserve_amount_title">Der zu sendende Betrag muss mindestens %s betragen</string>
@ -744,6 +761,8 @@
<string name="staking_notification_earn_rewards_text_period_week">Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jede Woche.</string>
<string name="staking_notification_earn_rewards_text_weekly">Sicher staken und wöchentliche Belohnungen verdienen.</string>
<string name="staking_notification_earn_rewards_title">Verdiene Staking-Belohnungen</string>
<string name="staking_notification_low_staked_balance_text">Dein verbleibender Stakingbetrag ist zu niedrig, um den Einsatz aufzuheben. Du musst mehr Staken, um den Mindestbetrag zum Aufheben des Einsatzes zu erreichen.</string>
<string name="staking_notification_low_staked_balance_title">Niedriges Stakingguthaben</string>
<string name="staking_notification_network_error_text">Aufgrund von Netzwerkproblemen ist Staking derzeit nicht verfügbar. Bitte versuche es später erneut.</string>
<string name="staking_notification_new_validator_funds_transfer">Beim Staking im %1$s -Netzwerk mit einem neuen Validator werden alle zuvor eingesetzten Kryptos automatisch an diesen Validator übertragen</string>
<string name="staking_notification_restake_rewards_text">Reinvestiert Deine verdienten Prämien in Deinen Einsatzbetrag und erhöht so den potenziellen Gewinn.</string>
@ -788,7 +807,7 @@
<string name="staking_unbonding">Lösen der Bindungen</string>
<string name="staking_unlocked_locked">Gelocktes unlocken</string>
<string name="staking_unlocking">Entsperren</string>
<string name="staking_unstake_amount_requirement_error">Die Anzahl der zu stakenden Krypros muss mindesten %s betragen</string>
<string name="staking_unstake_amount_requirement_error">Der unstaking-Betrag muss mindestens %s betragen</string>
<string name="staking_unstake_amount_validation_error">Der Betrag übersteigt das eingesetzte Guthaben</string>
<string name="staking_unstaked">Unstaken</string>
<string name="staking_unstaking">Staking beenden</string>
@ -815,9 +834,10 @@
<string name="swap_promo_text">Tausche mehr Token zu besseren Kursen direkt in deiner Brieftasche.</string>
<string name="swap_promo_title">Neuer Swap-Anbieter verfügbar!</string>
<string name="swapping_alert_cex_description">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.</string>
<string name="swapping_alert_cex_description_with_slippage">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</string>
<string name="swapping_alert_dex_description">Der Betrag enthält die Gebühren des Dienstleisters.</string>
<string name="swapping_alert_slippage_description">Anbieter-Slippage kann bis zu %s ausfallen.</string>
<string name="swapping_alert_title">Gebühren</string>
<string name="swapping_alert_dex_description_with_slippage">Der Betrag beinhaltet die Gebühr des Dienstleisters. \n\nProvider-Slippage kann bis zu %s</string>
<string name="swapping_alert_title">Information</string>
<string name="swapping_approve_information_text">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.</string>
<string name="swapping_approve_information_title">Genehmigen</string>
<string name="swapping_fee_estimation_error_text">Fehler bei der Gebührenschätzung. Bitte sende dein Feedback an den Support.</string>
@ -867,6 +887,8 @@
<string name="transaction_history_transaction_from_address">von: %s</string>
<string name="transaction_history_transaction_to_address">zu: %s</string>
<string name="transaction_history_transaction_validator">Validierer: %s</string>
<string name="transfer_min_amount_error">Minimum %s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">Der Mindesttransaktionsbetrag beträgt %1$s.</string>
<string name="try_to_load_data_again_button_title">Versuche es erneut</string>
<string name="twin_error_same_card">Du hast dieselbe Karte oder Ring gescannt. Um ein Zwillings-Wallet zu erstellen, musst du die Karte oder Ring mit der Nummer %d scannen.</string>
<string name="twin_error_wrong_twin">Du hast die falsche Doppelkarte oder Ring gescannt. Bitte versuche eine andere Karte oder Ring</string>
@ -1002,7 +1024,7 @@
<string name="warning_solana_fee_message">Das Solana-Netz ist überlastet. Wenn deine Transaktion nicht innerhalb von 2 Minuten bearbeitet wird, wiederhole bitte die Transaktion.</string>
<string name="warning_solana_fee_title">Solana Netzwerkalarm</string>
<string name="warning_solana_rent_fee_message">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.</string>
<string name="warning_some_networks_unreachable_message">Einige Netzwerke sind derzeit nicht erreichbar. Bitte versuche es später erneut.</string>
<string name="warning_some_networks_unreachable_message">Wischen Sie nach unten, um zu aktualisieren, oder versuchen Sie es später erneut.</string>
<string name="warning_some_networks_unreachable_title">Einige Netzwerke sind nicht erreichbar</string>
<string name="warning_testnet_card_message">Dies ist eine Testnet-Karte. Sie kann keine Transaktionen verarbeiten und sollte nur zu Test- und Entwicklungszwecken verwendet werden.</string>
<string name="warning_testnet_card_title">Nur für Testzwecke</string>

View file

@ -1,6 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="action_buttons_buy_empty_search_message">¿No encuentra el token en su billetera? Consulte los mercados para encontrarlo y añadirlo a la compra</string>
<string name="action_buttons_sell_empty_search_message">¿No encuentra el token en su billetera? Consulte los mercados para encontrarlo y añadirlo a la venta</string>
<string name="action_buttons_swap_choose_token">Elige el token</string>
<string name="action_buttons_swap_empty_search_message">¿No encuentras el token en tu billetera? Consulte los mercados para encontrarlo y agregarlo al intercambio</string>
<string name="action_buttons_swap_no_available_pair_notification_message">No hay tokens disponibles para intercambiar con el token seleccionado. Por favor elige otro.</string>
<string name="action_buttons_swap_no_available_pair_notification_title">No hay par disponible</string>
<string name="action_buttons_you_want_to_receive">Desea recibir</string>
<string name="action_buttons_you_want_to_swap">Quiere intercambiar</string>
<string name="add_custom_token_choose_network">Elige red</string>
@ -91,7 +96,7 @@
<string name="common_claim">Reclamar</string>
<string name="common_claim_rewards">Reclame recompensas</string>
<string name="common_close">Cerrar</string>
<string name="common_confirm">confirme</string>
<string name="common_confirm">Confirme</string>
<string name="common_continue">Continuar</string>
<string name="common_copy">Copiar</string>
<string name="common_copy_address">Copiar la dirección</string>
@ -554,9 +559,20 @@
<string name="onboarding_wallet_info_title_fourth">Restaurar código de acceso</string>
<string name="onboarding_wallet_info_title_second">Tarjetas idénticas</string>
<string name="onboarding_wallet_info_title_third">Código de acceso</string>
<string name="onramp_country_search">Buscar por país</string>
<string name="onramp_country_unavailable">Indisponible</string>
<string name="onramp_currency_search">Buscar por moneda</string>
<string name="onramp_max_amount_restriction">El monto de la compra no debe ser mayor a %s</string>
<string name="onramp_min_amount_restriction">La cantidad a comprar debe ser como mínimo %s</string>
<string name="onramp_no_available_providers">No hay proveedores disponibles para esta moneda</string>
<string name="onramp_pay_with">Pagar con</string>
<string name="onramp_residency_bottomsheet_country_not_supported">Nuestros servicios no están disponibles en este país</string>
<string name="onramp_residency_bottomsheet_country_subtitle">Cámbielo o confírmelo</string>
<string name="onramp_residency_bottomsheet_title">Su residencia ha sido identificada como</string>
<string name="onramp_settings_residence">Residencia</string>
<string name="onramp_settings_residence_description">Seleccione el país correcto para garantizar opciones de pago y servicios precisos.</string>
<string name="onramp_settings_title">Ajustes</string>
<string name="onramp_via">Vía</string>
<string name="organize_tokens_group">Agrupar</string>
<string name="organize_tokens_sort_by_balance">Por saldo</string>
<string name="organize_tokens_title">Organizar tokens</string>
@ -699,6 +715,7 @@
<string name="settings_wallet_name_title">Nombre</string>
<string name="staking_amount_requirement_error">El monto del staking debe ser al menos %s</string>
<string name="staking_amount_tron_integer_error">El monto del staking se redondeará a %1$s TRX debido a las reglas de la red.</string>
<string name="staking_amount_tron_integer_error_unstaking">El monto de cancelación del staking se redondeará a %1$s TRX debido a las reglas de la red.</string>
<string name="staking_claim_unstaked">Unstaking de la reclamación</string>
<string name="staking_details_account_fee">Tarifa de staking de la cuenta </string>
<string name="staking_details_account_fee_info">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.</string>
@ -725,7 +742,7 @@
<string name="staking_details_warmup_period">Periodo de calentamiento</string>
<string name="staking_details_warmup_period_info">El tiempo permitido para activar la participación en la apuesta.</string>
<string name="staking_give_permission_fee_footer">La red cobrará una tarifa de aprobación de token para verificar que usted está autorizando el uso de su token para el staking.</string>
<string name="staking_legal">Al utilizar la función de staking, usted acepta %1$s y %2$s del proveedor</string>
<string name="staking_legal">Al utilizar la función de staking, usted acepta %1$s y %2$s del proveedor</string>
<string name="staking_locked">Bloqueado</string>
<string name="staking_migrate">Migrar</string>
<string name="staking_native">Native staking</string>
@ -741,6 +758,8 @@
<string name="staking_notification_earn_rewards_text_period_week">El staking le permite ganar %1$s. Sus recompensas por apostar llegan todas las semanas.</string>
<string name="staking_notification_earn_rewards_text_weekly">Haga staking de forma segura y comience a ganar recompensas semanales</string>
<string name="staking_notification_earn_rewards_title">Gane recompensas por staking</string>
<string name="staking_notification_low_staked_balance_text">Su saldo de staking será demasiado bajo para cancelar el staking. Necesitará hacer staking más para alcanzar la cantidad mínima para cancelarlo.</string>
<string name="staking_notification_low_staked_balance_title">Saldo de staking bajo</string>
<string name="staking_notification_network_error_text">El staking no está disponible actualmente debido a las condiciones de la red. Inténtelo de nuevo más tarde.</string>
<string name="staking_notification_new_validator_funds_transfer">El staking en la red %1$s con un nuevo validador transferirá automáticamente todos los fondos previamente en staking a este validador.</string>
<string name="staking_notification_restake_rewards_text">Reinvierta las recompensas obtenidas en el monto apostado, aumentando las ganancias potenciales.</string>
@ -774,6 +793,7 @@
<string name="staking_rewards">Recompensas</string>
<string name="staking_stake_locked">El stake está bloqueado</string>
<string name="staking_stake_more">Hacer más staking</string>
<string name="staking_staked_amount">Monto del staking</string>
<string name="staking_summary_description_text">Ud hace el staking de %1$s y recibirá %2$s</string>
<string name="staking_tap_to_unlock">Toque para desbloquear</string>
<string name="staking_tap_to_unlock_or_vote">Pulse para desbloquear o votar</string>
@ -784,7 +804,7 @@
<string name="staking_unbonding">Desunión</string>
<string name="staking_unlocked_locked">Desbloquear</string>
<string name="staking_unlocking">Desbloqueando</string>
<string name="staking_unstake_amount_requirement_error">El monto del staking debe ser al menos %s</string>
<string name="staking_unstake_amount_requirement_error">La cantidad para el staking debe ser al menos %s</string>
<string name="staking_unstake_amount_validation_error">El monto excede el saldo apostado</string>
<string name="staking_unstaked">Sin staking</string>
<string name="staking_unstaking">Unstaking</string>
@ -811,7 +831,9 @@
<string name="swap_promo_text">Intercambie más tokens a mejores tasas directamente en su billetera.</string>
<string name="swap_promo_title">¡Nuevo proveedor de intercambio disponible!</string>
<string name="swapping_alert_cex_description">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.</string>
<string name="swapping_alert_cex_description_with_slippage">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</string>
<string name="swapping_alert_dex_description">El importe incluye los honorarios del proveedor de servicios.</string>
<string name="swapping_alert_dex_description_with_slippage">El importe incluye las tarifas del proveedor de servicios. \n\nEl slippage del proveedor puede alcanzar el %s</string>
<string name="swapping_alert_title">Tarifa</string>
<string name="swapping_approve_information_text">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.</string>
<string name="swapping_approve_information_title">Aprobar</string>

View file

@ -1,6 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="action_buttons_buy_empty_search_message">Jeton non trouvé dans votre portefeuille ? Consultez les marchés pour le trouver et l\'ajouter à l\'achat</string>
<string name="action_buttons_sell_empty_search_message">Jeton non trouvé dans votre portefeuille ? Consultez les marchés pour le trouver et l\'ajouter à la vente</string>
<string name="action_buttons_swap_choose_token">Choisissez le jeton</string>
<string name="action_buttons_swap_empty_search_message">Jeton non trouvé dans votre portefeuille ? Consultez les marchés pour le trouver et l\'ajouter à l\'échange</string>
<string name="action_buttons_swap_no_available_pair_notification_message">Il n\'y a pas de token disponible pour échanger avec le token sélectionné. Veuillez en choisir un autre.</string>
<string name="action_buttons_swap_no_available_pair_notification_title">Aucune paire disponible</string>
<string name="action_buttons_you_want_to_receive">Vous souhaitez recevoir</string>
<string name="action_buttons_you_want_to_swap">Vous souhaitez échanger</string>
<string name="add_custom_token_choose_network">Choisissez le réseau</string>
@ -554,9 +559,20 @@
<string name="onboarding_wallet_info_title_fourth">Restauration du code d\'accès</string>
<string name="onboarding_wallet_info_title_second">Cartes identiques</string>
<string name="onboarding_wallet_info_title_third">Code d\'accès</string>
<string name="onramp_country_search">Recherche par pays</string>
<string name="onramp_country_unavailable">Indisponible</string>
<string name="onramp_currency_search">Recherche par devise</string>
<string name="onramp_max_amount_restriction">Le montant de l\'achat ne doit pas dépasser %s</string>
<string name="onramp_min_amount_restriction">Le montant à acheter doit être au moins %s</string>
<string name="onramp_no_available_providers">Aucun fournisseur disponible pour cette devise</string>
<string name="onramp_pay_with">Payer avec</string>
<string name="onramp_residency_bottomsheet_country_not_supported">Nos services ne sont pas disponibles dans ce pays</string>
<string name="onramp_residency_bottomsheet_country_subtitle">Modifiez-le ou confirmez-le</string>
<string name="onramp_residency_bottomsheet_title">Votre résidence a été identifiée comme</string>
<string name="onramp_settings_residence">Résidence</string>
<string name="onramp_settings_residence_description">Veuillez sélectionner le bon pays pour garantir des options de paiement et des services précis.</string>
<string name="onramp_settings_title">Paramètres</string>
<string name="onramp_via">Via</string>
<string name="organize_tokens_group">Grouper</string>
<string name="organize_tokens_sort_by_balance">Par solde</string>
<string name="organize_tokens_title">Organiser les jetons</string>
@ -699,6 +715,7 @@
<string name="settings_wallet_name_title">Nom</string>
<string name="staking_amount_requirement_error">Le montant à staker doit être au moins %s</string>
<string name="staking_amount_tron_integer_error">Le montant du staking sera arrondi à %1$s TRX en raison des règles du réseau.</string>
<string name="staking_amount_tron_integer_error_unstaking">Le montant d\'annulation du staking sera arrondi à %1$s TRX en raison des règles du réseau.</string>
<string name="staking_claim_unstaked">Réclamation déstakée</string>
<string name="staking_details_account_fee">Frais de staking du compte</string>
<string name="staking_details_account_fee_info">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é.</string>
@ -725,7 +742,7 @@
<string name="staking_details_warmup_period">Période d\'échauffement</string>
<string name="staking_details_warmup_period_info">Le temps imparti pour activer la participation au staking.</string>
<string name="staking_give_permission_fee_footer">Le réseau facturera des frais dapprobation de jeton pour vérifier que vous autorisez lutilisation de votre jeton pour le jalonnement.</string>
<string name="staking_legal">En utilisant la fonctionnalité de staking, vous acceptez les %1$s et %2$s du fournisseur</string>
<string name="staking_legal">En utilisant la fonctionnalité de staking, vous acceptez les %1$s et %2$s du fournisseur</string>
<string name="staking_locked">Bloqué</string>
<string name="staking_migrate">Migrer</string>
<string name="staking_native">Native staking</string>
@ -741,6 +758,8 @@
<string name="staking_notification_earn_rewards_text_period_week">Le staking vous permet de gagner %1$s. Vos récompenses de staking arrivent toutes les semaines.</string>
<string name="staking_notification_earn_rewards_text_weekly">Stakez en toute sécurité et commencez à gagner des récompenses hebdomadaires</string>
<string name="staking_notification_earn_rewards_title">Gagnez des récompenses de staking</string>
<string name="staking_notification_low_staked_balance_text">Votre solde stakée sera trop faible pour annuler votre staking. Vous devrez staker davantage pour atteindre le montant minimum pour annuler le staking.</string>
<string name="staking_notification_low_staked_balance_title">Solde de staking faible</string>
<string name="staking_notification_network_error_text">L\'option de staking n\'est actuellement pas disponible en raison des conditions du réseau. Veuillez réessayer plus tard.</string>
<string name="staking_notification_new_validator_funds_transfer">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</string>
<string name="staking_notification_restake_rewards_text">Réinvestissez vos récompenses gagnées dans le montant que vous avez staké, augmentant ainsi vos gains potentiels.</string>
@ -774,6 +793,7 @@
<string name="staking_rewards">Récompenses</string>
<string name="staking_stake_locked">Stake verrouillé</string>
<string name="staking_stake_more">Staker plus</string>
<string name="staking_staked_amount">Montant staké</string>
<string name="staking_summary_description_text">Vous stakez %1$s et recevrez %2$s</string>
<string name="staking_tap_to_unlock">Appuyez pour déverrouiller</string>
<string name="staking_tap_to_unlock_or_vote">Appuyez pour déverrouiller ou voter</string>
@ -784,7 +804,7 @@
<string name="staking_unbonding">Dissociation</string>
<string name="staking_unlocked_locked">Débloquer</string>
<string name="staking_unlocking">Déverrouillage</string>
<string name="staking_unstake_amount_requirement_error">Le montant à staker doit être au moins %s</string>
<string name="staking_unstake_amount_requirement_error">Le montant à destaker doit être au moins %s</string>
<string name="staking_unstake_amount_validation_error">Le montant dépasse le solde misé</string>
<string name="staking_unstaked">Non-staké</string>
<string name="staking_unstaking">Unstaking</string>
@ -811,7 +831,9 @@
<string name="swap_promo_text">Échangez plus de jetons à de meilleurs taux directement dans votre portefeuille.</string>
<string name="swap_promo_title">Nouveau fournisseur d\'échange disponible !</string>
<string name="swapping_alert_cex_description">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.</string>
<string name="swapping_alert_cex_description_with_slippage">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</string>
<string name="swapping_alert_dex_description">Le montant comprend les frais du fournisseur de services.</string>
<string name="swapping_alert_dex_description_with_slippage">Le montant comprend les frais du fournisseur de services. \n\nLe slippage du fournisseur peut atteindre %s</string>
<string name="swapping_alert_title">Frais</string>
<string name="swapping_approve_information_text">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.</string>
<string name="swapping_approve_information_title">Approuver</string>

View file

@ -1,6 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="action_buttons_buy_empty_search_message">ポートフォリオにトークンが見つかりませんか?マーケットから見つけ、買付のために追加してください</string>
<string name="action_buttons_sell_empty_search_message">ポートフォリオにトークンが見つかりませんか?マーケットから見つけ、売却のために追加してください</string>
<string name="action_buttons_swap_choose_token">トークンを選択</string>
<string name="action_buttons_swap_empty_search_message">ポートフォリオにトークンが見つかりませんか?マーケットから見つけ、スワップのために追加してください</string>
<string name="action_buttons_swap_no_available_pair_notification_message">選択したトークンと交換できるトークンがありません。別のトークンを選択してください。</string>
<string name="action_buttons_swap_no_available_pair_notification_title">利用可能なペアがありません</string>
<string name="action_buttons_you_want_to_receive">受け取りたい</string>
<string name="action_buttons_you_want_to_swap">交換したい</string>
<string name="add_custom_token_choose_network">ネットワークを選択</string>
@ -442,6 +447,7 @@
<string name="markets_token_details_price_performance">値動き</string>
<string name="markets_token_details_repository">リポジトリ</string>
<string name="markets_token_details_security_score">セキュリティ・スコア</string>
<string name="markets_token_details_security_score_description">トークンのセキュリティ・スコアとは、ブロックチェーンやトークンのセキュリティ・レベルを様々な要因に基づいて評価する指標で、以下の情報源から集計されます。</string>
<string name="markets_token_details_social">ソーシャル</string>
<string name="markets_token_details_total_supply">総供給量</string>
<string name="markets_token_details_total_supply_description">特定の暗号資産に存在しうるコインまたはトークンの最大数</string>
@ -546,10 +552,21 @@
<string name="onboarding_wallet_info_title_fourth">アクセスコードの復元</string>
<string name="onboarding_wallet_info_title_second">同一のカード</string>
<string name="onboarding_wallet_info_title_third">アクセスコード</string>
<string name="onramp_country_search">国で検索</string>
<string name="onramp_country_unavailable">利用不可</string>
<string name="onramp_currency_search">通貨で検索</string>
<string name="onramp_max_amount_restriction">買付金額は%s以下にしてください</string>
<string name="onramp_min_amount_restriction">買付金額は少なくとも%sである必要があります</string>
<string name="onramp_no_available_providers">この通貨で利用可能なプロバイダーはありません</string>
<string name="onramp_pay_with">支払う</string>
<string name="onramp_redirecting_to_provider_subtitle">サードパーティプロバイダー%sで取引を完了できます。</string>
<string name="onramp_redirecting_to_provider_title">%sにリダイレクトしています...</string>
<string name="onramp_residency_bottomsheet_country_not_supported">この国では当社のサービスはご利用いただけません</string>
<string name="onramp_residency_bottomsheet_country_subtitle">変更または確認</string>
<string name="onramp_residency_bottomsheet_title">あなたの住居は次のように識別されています</string>
<string name="onramp_settings_residence">住居</string>
<string name="onramp_settings_residence_description">正確なお支払い方法とサービスを確保するため、正しい国を選択してください。</string>
<string name="onramp_settings_title">設定</string>
<string name="onramp_via">経由</string>
<string name="organize_tokens_group">グループ</string>
<string name="organize_tokens_sort_by_balance">残高順</string>
@ -734,6 +751,8 @@
<string name="staking_notification_earn_rewards_text_period_week">ステーキングにより%1$sを獲得できます。ステーキング報酬は毎週受け取れます。</string>
<string name="staking_notification_earn_rewards_text_weekly">安全にステーキングして、報酬を毎週獲得しましょう</string>
<string name="staking_notification_earn_rewards_title">ステーキング報酬を獲得</string>
<string name="staking_notification_low_staked_balance_text">残りのステーキング残高が少なすぎてステーキングを解除できません。最小のステーキング解除残高を満たすには、さらにステーキングする必要があります。</string>
<string name="staking_notification_low_staked_balance_title">ステーキング残高が低いです</string>
<string name="staking_notification_network_error_text">ネットワークの状態により、現在ステーキングはご利用いただけません。しばらくしてからもう一度お試しください。</string>
<string name="staking_notification_new_validator_funds_transfer">新しいバリデーターで%1$sネットワークにステーキングすると、以前にステーキングされた資金はすべてこのバリデーターに自動的に転送されます。</string>
<string name="staking_notification_restake_rewards_text">獲得した報酬をステーキングに再投資し、潜在的な収益を増やします。</string>
@ -778,7 +797,7 @@
<string name="staking_unbonding">ステーキング解約中</string>
<string name="staking_unlocked_locked">ロック解除</string>
<string name="staking_unlocking">ロック解除中</string>
<string name="staking_unstake_amount_requirement_error">ステーキング金額は %s 以上である必要があります</string>
<string name="staking_unstake_amount_requirement_error">ステーキング解除する金額は少なくとも%sである必要があります</string>
<string name="staking_unstake_amount_validation_error">金額がステーキング残高を超えています</string>
<string name="staking_unstaked">ステーキングされていない</string>
<string name="staking_unstaking">ステーキング解除</string>
@ -805,8 +824,9 @@
<string name="swap_promo_text">より多くのトークンをより良いレートで、ウォレット内にて直接交換します。</string>
<string name="swap_promo_title">新しいスワッププロバイダーが利用可能になりました!</string>
<string name="swapping_alert_cex_description">この金額には以下が含まれます:\n- サービスプロバイダーの手数料\n- 取引所からユーザーのアドレスに%s を送り返すためのネットワーク手数料。</string>
<string name="swapping_alert_cex_description_with_slippage">金額には以下が含まれます: \n • サービス プロバイダーの手数料\n • 取引所からユーザーのアドレスに%1$sを送金するためのネットワーク手数料。 \n\nプロバイダーのスリッページは最大%2$sです</string>
<string name="swapping_alert_dex_description">この金額には、サービスプロバイダーの手数料が含まれています。</string>
<string name="swapping_alert_slippage_description">プロバイダーのスリッページは最大%sです</string>
<string name="swapping_alert_dex_description_with_slippage">金額にはサービスプロバイダーの手数料が含まれます。 \n\nプロバイダーのスリッページは最大%s です</string>
<string name="swapping_alert_title">手数料</string>
<string name="swapping_approve_information_text">すべての分散型取引所は、スマートコントラクトがあなたの許可なくウォレットにアクセスするのを防ぐために承認を必要とします。設計上、スマートコントラクトは承認なしでトークンにアクセスできません。トークンを「ロック解除」することで、あなたは1-inchのスマートコントラクトがトークンを使うことを承認します。ネットワークのマイナーは、このアクションをブロックチェーンに記録するためのガス料金あなたが支払うを受け取ります。承認後、トークンを交換することができます。</string>
<string name="swapping_approve_information_title">承認</string>
@ -857,6 +877,8 @@
<string name="transaction_history_transaction_from_address">送金元: %s</string>
<string name="transaction_history_transaction_to_address">送金先: %s</string>
<string name="transaction_history_transaction_validator">バリデーター: %s</string>
<string name="transfer_min_amount_error">最小%s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">最小取引金額は%1$sです。</string>
<string name="try_to_load_data_again_button_title">もう一度やり直してください</string>
<string name="twin_error_same_card">同じカードをスキャンしました。ツインウォレットを作成するには、番号%dのカードをスキャンする必要があります。</string>
<string name="twin_error_wrong_twin">間違ったツインカードをスキャンしました。別のカードをお試しください。</string>

View file

@ -1,6 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="action_buttons_swap_choose_token">Выберите токен</string>
<string name="action_buttons_swap_no_available_pair_notification_message">Нет доступных токенов для обмена с выбранным токеном, пожалуйста, выберите другой.</string>
<string name="action_buttons_swap_no_available_pair_notification_title">Нет доступных пар</string>
<string name="action_buttons_you_want_to_receive">Вы хотите получить</string>
<string name="action_buttons_you_want_to_swap">Вы хотите обменять</string>
<string name="add_custom_token_choose_network">Выберите сеть</string>
@ -311,7 +313,7 @@
<string name="give_permission_current_transaction">Транзакция</string>
<string name="give_permission_policy_type_footer">Укажите лимит доступа к выбранному токену</string>
<string name="give_permission_rows_amount">Количество %s</string>
<string name="give_permission_staking_footer">Функция подтверждения необходима для предоставления другому адресу разрешения на использование определенного количества ваших токенов.По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту StakeKit разрешение использовать ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете осуществить стейкинг токена.</string>
<string name="give_permission_staking_footer">Функция подтверждения необходима для предоставления другому адресу разрешения на использование определенного количества ваших токенов. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту StakeKit разрешение использовать ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете осуществить стейкинг токена.</string>
<string name="give_permission_staking_subtitle">Чтобы продолжить, вам необходимо разрешить смарт контракту Polygon использовать ваш %s</string>
<string name="give_permission_swap_subtitle" formatted="false">Чтобы продолжить, вам нужно разрешить смарт-контракту %1s использовать ваш %2s</string>
<string name="give_permission_title">Дать разрешение</string>
@ -569,7 +571,14 @@
<string name="onboarding_wallet_info_title_fourth">Восстановление кода доступа</string>
<string name="onboarding_wallet_info_title_second">Идентичные карты</string>
<string name="onboarding_wallet_info_title_third">Код доступа</string>
<string name="onramp_country_search">Поиск по стране</string>
<string name="onramp_country_unavailable">Недоступно</string>
<string name="onramp_currency_search">Поиск по валюте</string>
<string name="onramp_max_amount_restriction">Сумма покупки не может быть больше, чем %s</string>
<string name="onramp_min_amount_restriction">Сумма покупки должна составлять минимум %s</string>
<string name="onramp_pay_with">Оплата с</string>
<string name="onramp_residency_bottomsheet_country_not_supported">Наши сервисы недоступны в данной стране</string>
<string name="onramp_settings_title">Настройки</string>
<string name="onramp_via">Через</string>
<string name="organize_tokens_group">Группы</string>
<string name="organize_tokens_sort_by_balance">По балансу</string>
@ -761,6 +770,7 @@
<string name="staking_notification_earn_rewards_text_weekly">Стейкайте безопасно и начинайте получать еженедельные награды.</string>
<string name="staking_notification_earn_rewards_title">Получите награду за стейкинг</string>
<string name="staking_notification_low_staked_balance_text">Оставшийся застейканный баланс будет слишком мал для вывода. Вам потребуется застейкать больше средств, чтобы достичь минимальной суммы для вывода.</string>
<string name="staking_notification_low_staked_balance_title">Низкий баланс стейкинга</string>
<string name="staking_notification_network_error_text">Стейкинг временно недоступен из-за проблем в сети. Пожалуйста, попробуйте позже.</string>
<string name="staking_notification_new_validator_funds_transfer">Стейкинг в сети %1$s с новым валидатором автоматически переведет ваши текущие застейканные средства на него.</string>
<string name="staking_notification_restake_rewards_text">Реинвестируйте свои заработанные награды в вашу застейканную сумму, увеличивая потенциальный доход</string>
@ -793,6 +803,7 @@
<string name="staking_rewards">Вознаграждения</string>
<string name="staking_stake_locked">Стейкинг закрыт</string>
<string name="staking_stake_more">Застейкать еще</string>
<string name="staking_staked_amount">Застейканная сумма</string>
<string name="staking_summary_description_text">Вы стейкаете %1$s и будете получать награду %2$s</string>
<string name="staking_tap_to_unlock">Нажмите для разблокировки</string>
<string name="staking_tap_to_unlock_or_vote">Нажмите для разблокировки</string>
@ -830,8 +841,9 @@
<string name="swap_promo_text">Обменивайте больше токенов по лучшим курсам прямо в вашем кошельке.</string>
<string name="swap_promo_title">Новый провайдер обмена!</string>
<string name="swapping_alert_cex_description">В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя.</string>
<string name="swapping_alert_cex_description_with_slippage">В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %1$s от биржи обратно на адрес пользователя \n\nПроскальзывание провайдера составляет до %2$s</string>
<string name="swapping_alert_dex_description">В сумму включена комиссия провайдера сервиса.</string>
<string name="swapping_alert_slippage_description">Проскальзывание провайдера составляет до %s.</string>
<string name="swapping_alert_dex_description_with_slippage">В сумму включена комиссия провайдера сервиса. \n\nПроскальзывание провайдера составляет до %s</string>
<string name="swapping_alert_title">Информация</string>
<string name="swapping_approve_information_text">Подтверждения считаются отраслевым стандартом для всех децентрализованных бирж и защищают ваш кошелек от доступа со стороны смарт-контракта без вашего разрешения. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту 1inch разрешение тратить ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете обменять свой токен.</string>
<string name="swapping_approve_information_title">Подтвердить</string>
@ -925,7 +937,7 @@
<string name="wallet_connect_error_with_framework_message">Произошла непредвиденная ошибка. Сообщение ошибки: %s Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки.</string>
<string name="wallet_connect_error_wrong_card_selected">Неверная карта или кольцо выбрана в приложении Tangem</string>
<string name="wallet_connect_failed_to_build_tx">Не удалось создать транзакцию из данных Dapp. Код: %s</string>
<string name="wallet_connect_generic_error_with_code">Произошла непредвиденная ошибка. Код ошибки: %d. Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки.</string>
<string name="wallet_connect_generic_error_with_code">Произошла непредвиденная ошибка. Код ошибки: %d. Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать обратитесь в службу поддержки.</string>
<string name="wallet_connect_no_sessions_message">Нет открытых сессий WalletConnect</string>
<string name="wallet_connect_no_sessions_title">Упс. Нет сессий.</string>
<string name="wallet_connect_pairing_error">Не удалось создать пару WalletConnect: %1$s</string>
@ -980,7 +992,7 @@
<string name="warning_express_notification_invalid_reserve_amount_title">Сумма получения не может быть менее %s</string>
<string name="warning_express_pair_unavailable_message">Это может произойти из-за того, что провайдер временно не предоставляет обмен выбранной вами пары. Пожалуйста, подождите некоторое время и попробуйте снова. (Код %s)</string>
<string name="warning_express_pair_unavailable_title">Выбранная пара временно недоступна</string>
<string name="warning_express_refresh_required_title">Cервис временно недоступен</string>
<string name="warning_express_refresh_required_title">Сервис временно недоступен</string>
<string name="warning_express_too_maximum_amount_title">Сумма для обмена должна быть не более %s</string>
<string name="warning_express_too_minimal_amount_title">Сумма для обмена должна быть не менее %s</string>
<string name="warning_express_wrong_amount_description">Пожалуйста, измените сумму для обмена</string>
@ -1019,7 +1031,7 @@
<string name="warning_solana_fee_message">Сеть Солана испытывает высокую нагрузку. Если Ваша транзакция не прошла в течение 2 минут, повторите её отправку.</string>
<string name="warning_solana_fee_title">Оповещение сети Солана</string>
<string name="warning_solana_rent_fee_message">Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату.</string>
<string name="warning_some_networks_unreachable_message">Некоторые сети в настоящее время недоступны. Пожалуйста, повторите попытку позже.</string>
<string name="warning_some_networks_unreachable_message">Свайпните вниз для обновления или попробуйте позже.</string>
<string name="warning_some_networks_unreachable_title">Некоторые сети недоступны</string>
<string name="warning_testnet_card_message">Это Testnet карта. Он не может обрабатывать транзакции и используется только в целях тестирования и разработки.</string>
<string name="warning_testnet_card_title">Только для целей тестирования</string>

View file

@ -1,6 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="action_buttons_buy_empty_search_message">Токен не знайдено у вашому портфелі? Перевірте Ринки, щоб знайти та додати його для покупки</string>
<string name="action_buttons_sell_empty_search_message">Токен не знайдено у вашому портфелі? Перевірте Ринки, щоб знайти та додати його для продажу</string>
<string name="action_buttons_swap_choose_token">Оберіть токен</string>
<string name="action_buttons_swap_empty_search_message">Токен не знайдено у вашому портфелі? Перевірте ринки, щоб знайти та додати його для обміну</string>
<string name="action_buttons_swap_no_available_pair_notification_message">Немає доступних токенів для обміну з обраним токеном. Будь ласка, оберіть інший.</string>
<string name="action_buttons_swap_no_available_pair_notification_title">Немає вільної пари</string>
<string name="action_buttons_you_want_to_receive">Ви хочете отримати</string>
<string name="action_buttons_you_want_to_swap">Ви хочете обміняти</string>
<string name="add_custom_token_choose_network">Оберіть мережу</string>
@ -523,7 +528,7 @@
</plurals>
<string name="onboarding_seed_generate_title">Ваша seed-фраза</string>
<plurals name="onboarding_seed_generate_words_count">
<item quantity="one">слово</item>
<item quantity="one"></item>
<item quantity="few">%d слова</item>
<item quantity="many">%d слів</item>
<item quantity="other">%d слів</item>
@ -570,9 +575,16 @@
<string name="onboarding_wallet_info_title_fourth">Відновлення коду доступу</string>
<string name="onboarding_wallet_info_title_second">Ідентичні картки</string>
<string name="onboarding_wallet_info_title_third">Код доступу</string>
<string name="onramp_country_search">Пошук за країною</string>
<string name="onramp_country_unavailable">Недоступно</string>
<string name="onramp_currency_search">Пошук по валюті</string>
<string name="onramp_pay_with">Оплата з</string>
<string name="onramp_residency_bottomsheet_country_not_supported">Наші сервіси недоступні в цій країні</string>
<string name="onramp_residency_bottomsheet_country_subtitle">Змінити або підтвердити</string>
<string name="onramp_residency_bottomsheet_title">Ваше місце проживання визначено як</string>
<string name="onramp_settings_residence_description">Будь ласка, виберіть правильну країну, щоб забезпечити точні способи оплати та послуги.</string>
<string name="onramp_settings_title">Параметри</string>
<string name="onramp_via">Через</string>
<string name="organize_tokens_group">Групами</string>
<string name="organize_tokens_sort_by_balance">За балансом</string>
<string name="organize_tokens_title">Сортування токенів</string>
@ -719,9 +731,10 @@
<string name="settings_wallet_name_title">Ім\'я</string>
<string name="staking_amount_requirement_error">Сума для стейкінгу має бути не менше %s</string>
<string name="staking_amount_tron_integer_error">Сума стейкінгу буде округлена до %1$s TRX відповідно до правил мережі.</string>
<string name="staking_amount_tron_integer_error_unstaking">Сума зняття зі стейкінгу буде округлена до %1$s TRX через мережеві правила.</string>
<string name="staking_claim_unstaked">Зняти кошти</string>
<string name="staking_details_account_fee">Комісія за стейкінг-акаунт</string>
<string name="staking_details_account_fee_info">Стейкінг-акаунт - це спеціальний рахунок, на якому зберігаються застейкані SOL токени. Він створюється, коли ви делегуєте свої токени валідатору для участі у перевірці транзакцій та отримання винагород. За створення стейкінг-акаунту стягується невелика комісія, яка повертається після завершення стейкінгу.</string>
<string name="staking_details_account_fee_info">Стейкінг-акаунт це спеціальний рахунок, на якому зберігаються застейкані SOL токени. Він створюється, коли ви делегуєте свої токени валідатору для участі у перевірці транзакцій та отримання винагород. За створення стейкінг-акаунту стягується невелика комісія, яка повертається після завершення стейкінгу.</string>
<string name="staking_details_annual_percentage_rate">Процентна ставка</string>
<string name="staking_details_annual_percentage_rate_info">Річний відсоток, який ви можете отримати, беручи участь у стейкінгу.</string>
<string name="staking_details_apr">APR</string>
@ -761,10 +774,13 @@
<string name="staking_notification_earn_rewards_text_period_week">Стейкінг дає змогу вам отримувати %1$s. Винагорода буде зараховуватися кожен тиждень.</string>
<string name="staking_notification_earn_rewards_text_weekly">Стейкайте безпечно та почніть отримувати винагороди щотижня</string>
<string name="staking_notification_earn_rewards_title">Отримуйте винагороду за стейкінг</string>
<string name="staking_notification_low_staked_balance_text">Ваш баланс стейкінгу, що залишився, буде занадто низьким, щоб вивести його зі стейкінгу. Вам потрібно буде застейкати більше, щоб досягти мінімальної суми для виводу.</string>
<string name="staking_notification_low_staked_balance_title">Низький баланс стейкінгу</string>
<string name="staking_notification_network_error_text">Стейкінг тимчасово недоступний через проблеми в мережі. Будь ласка, спробуйте пізніше.</string>
<string name="staking_notification_new_validator_funds_transfer">Стейкінг в мережі %1$s з новим валідатором автоматично переведе всі раніше застейкані кошти до цього валідатора</string>
<string name="staking_notification_restake_rewards_text">Реінвестуйте зароблені винагороди у суму стейкінгу, щоб збільшити потенційний прибуток.</string>
<string name="staking_notification_restake_text">Рестейк дозволяє вам перемістити ваші кошти від одного валідатора до іншого без необхідності виводити кошти зі стейкінгу</string>
<string name="staking_notification_stake_entire_balance_text">Ви збираєтеся застейкати весь свій баланс. Ми рекомендуємо залишити невелику суму, щоб покрити комісію мережі за зняття коштів або отримання винагороди.</string>
<string name="staking_notification_unlock_text">Розблокуйте свої кошти, щоб вивести їх зі стейкінгу. Розблокування займе %s.</string>
<string name="staking_notification_unstake_cosmos_text">Ваші кошти будуть доступні для використання після закінчення 21-денного періоду розблокування. Винагорода буде отримана разом з вашими незастейканими коштами.</string>
<string name="staking_notification_unstake_text">Ваші кошти будуть доступні після %s періоду розблокування.</string>
@ -793,6 +809,7 @@
<string name="staking_rewards">Винагороди</string>
<string name="staking_stake_locked">Стейкінг закрито</string>
<string name="staking_stake_more">Застейкати більше</string>
<string name="staking_staked_amount">Застейканий залишок</string>
<string name="staking_summary_description_text">Ви стейкаєте %1$s і будете отримуватиме винагороду %2$s</string>
<string name="staking_tap_to_unlock">Натисніть, щоб розблокувати</string>
<string name="staking_tap_to_unlock_or_vote">Натисніть, щоб розблокувати або проголосувати</string>
@ -803,7 +820,6 @@
<string name="staking_unbonding">Розблокування</string>
<string name="staking_unlocked_locked">Розблокувати</string>
<string name="staking_unlocking">Розблокування</string>
<string name="staking_unstake_amount_requirement_error">Сума для стейкінгу має бути не менше %s</string>
<string name="staking_unstake_amount_validation_error">Сума перевищує баланс стейкінгу</string>
<string name="staking_unstaked">Вивід зі стейкінгу</string>
<string name="staking_unstaking">Зняти зі стейкінгу</string>
@ -830,7 +846,9 @@
<string name="swap_promo_text">Обмінюйте більше токенів за вигіднішим курсом прямо у своєму гаманці.</string>
<string name="swap_promo_title">З\'явився новий провайдер обмінів!</string>
<string name="swapping_alert_cex_description">Сума включає: \n• комісію постачальника послуг\n• комісію мережі за відправлення %s з біржі назад на адресу користувача.</string>
<string name="swapping_alert_cex_description_with_slippage">У суму входить:\n- комісія провайдера\n- мережева комісія за відправку %1$s з біржі назад на адресу користувача. \n\nПроскакування провайдера становить до %2$s</string>
<string name="swapping_alert_dex_description">Сума включає комісію постачальника послуг.</string>
<string name="swapping_alert_dex_description_with_slippage">Сума включає комісію провайдера послуг. \n\nПроскакування провайдера до %s</string>
<string name="swapping_alert_title">Комісії</string>
<string name="swapping_approve_information_text">Всі децентралізовані біржі вимагають схвалення, щоб запобігти доступу смарт-контрактів до вашого гаманця без вашого дозволу. За задумом смарт-контракти не можуть отримати доступ до ваших токенів без вашого схвалення. \"Розблоковуючи\" свої токени, ви дозволяєте смарт-контракту 1inch витрачати ваші активи. Майнери мережі отримують плату за газ (сплачену вами), щоб зафіксувати цю дію в блокчейні. Ви можете обміняти свій токен після того, як дасте дозвіл.</string>
<string name="swapping_approve_information_title">Підтвердити</string>
@ -881,6 +899,7 @@
<string name="transaction_history_transaction_from_address">від: %s</string>
<string name="transaction_history_transaction_to_address">до: %s</string>
<string name="transaction_history_transaction_validator">валідатор: %s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">Мінімальна сума транзакції становить %1$s.</string>
<string name="try_to_load_data_again_button_title">Спробуйте знову</string>
<string name="twin_error_same_card">Ви відсканували одну й ту саму картку. Для створення twin-гаманця вам потрібно відсканувати картку з номером %d</string>
<string name="twin_error_wrong_twin">Ви відсканували не ту twin-картку. Будь ласка, спробуйте відсканувати іншу</string>
@ -1018,7 +1037,7 @@
<string name="warning_solana_fee_message">Мережа Солана зазнає високого навантаження. Якщо транзакція не пройшла протягом 2 хвилин, повторіть транзакцію.</string>
<string name="warning_solana_fee_title">Оповіщення мережі Солана</string>
<string name="warning_solana_rent_fee_message">Мережа Solana стягує орендну плату у розмірі %1$s кожні 2 дні. Акаунти, які не можуть дозволити собі орендну плату, видаляються з мережі. Поповніть свій рахунок на суму понад %2$s, щоб не платити орендну плату.</string>
<string name="warning_some_networks_unreachable_message">Деякі мережі наразі недоступні. Будь ласка, спробуйте пізніше.</string>
<string name="warning_some_networks_unreachable_message">Проведіть пальцем вниз для оновлення або спробуйте пізніше.</string>
<string name="warning_some_networks_unreachable_title">Деякі мережі недоступні</string>
<string name="warning_testnet_card_message">Це картка Testnet. Вона не може обробляти транзакції і повинна використовуватися лише для тестування та розробки.</string>
<string name="warning_testnet_card_title">Лише для цілей тестування</string>

View file

@ -1,6 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="action_buttons_buy_empty_search_message">Token not found in your portfolio? Check the Markets to find and add it for purchase</string>
<string name="action_buttons_sell_empty_search_message">Token not found in your portfolio? Check the Markets to find and add it for sell</string>
<string name="action_buttons_swap_choose_token">Choose the Token</string>
<string name="action_buttons_swap_empty_search_message">Token not found in your portfolio? Check the Markets to find and add it for swap</string>
<string name="action_buttons_swap_no_available_pair_notification_message">There are no available tokens to swap with the selected token. Please choose another one.</string>
<string name="action_buttons_swap_no_available_pair_notification_title">No available pair</string>
<string name="action_buttons_you_want_to_receive">You want to Receive</string>
<string name="action_buttons_you_want_to_swap">You want to Swap</string>
<string name="add_custom_token_choose_network">Choose network</string>
@ -448,6 +453,7 @@
<string name="markets_token_details_price_performance">Price performance</string>
<string name="markets_token_details_repository">Repository</string>
<string name="markets_token_details_security_score">Security score</string>
<string name="markets_token_details_security_score_description">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.</string>
<string name="markets_token_details_social">Social</string>
<string name="markets_token_details_total_supply">Total supply</string>
<string name="markets_token_details_total_supply_description">The maximum number of coins or tokens that can ever exist for a particular cryptocurrency</string>
@ -557,7 +563,12 @@
<string name="onramp_country_search">Search by country</string>
<string name="onramp_country_unavailable">Unavailable</string>
<string name="onramp_currency_search">Search by currency</string>
<string name="onramp_max_amount_restriction">The purchase amount should be no more than %s</string>
<string name="onramp_min_amount_restriction">The amount to buy must be at least %s</string>
<string name="onramp_no_available_providers">No available providers for this currency</string>
<string name="onramp_pay_with">Pay with</string>
<string name="onramp_redirecting_to_provider_subtitle">You will be able to complete your transaction on the third-party provider, %s</string>
<string name="onramp_redirecting_to_provider_title">Redirecting to %s...</string>
<string name="onramp_residency_bottomsheet_country_not_supported">Our services are not available in this country</string>
<string name="onramp_residency_bottomsheet_country_subtitle">Change or confirm it</string>
<string name="onramp_residency_bottomsheet_title">Your residence has been identified as</string>
@ -823,8 +834,9 @@
<string name="swap_promo_text">Exchange more tokens at better rates directly in your wallet.</string>
<string name="swap_promo_title">New Swap Provider Available!</string>
<string name="swapping_alert_cex_description">The amount includes:\n• service provider\'s fee\n• network fee for sending %s from the exchange back to the user\'s address.</string>
<string name="swapping_alert_cex_description_with_slippage">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</string>
<string name="swapping_alert_dex_description">The amount includes the service provider\'s fee.</string>
<string name="swapping_alert_slippage_description">Provider slippage is up to %s.</string>
<string name="swapping_alert_dex_description_with_slippage">The amount includes the service provider\'s fee. \n\nProvider slippage is up to %s</string>
<string name="swapping_alert_title">Information</string>
<string name="swapping_approve_information_text">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.</string>
<string name="swapping_approve_information_title">Approve</string>
@ -875,6 +887,7 @@
<string name="transaction_history_transaction_from_address">from: %s</string>
<string name="transaction_history_transaction_to_address">to: %s</string>
<string name="transaction_history_transaction_validator">validator: %s</string>
<string name="transfer_min_amount_error">Minimum %s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">The minimum transaction amount is %1$s.</string>
<string name="try_to_load_data_again_button_title">Try again</string>
<string name="twin_error_same_card">You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d</string>
@ -1011,7 +1024,7 @@
<string name="warning_solana_fee_message">The Solana network is congested. If your transaction is not processed within 2 minutes, please repeat the transaction.</string>
<string name="warning_solana_fee_title">Solana Network Alert</string>
<string name="warning_solana_rent_fee_message">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.</string>
<string name="warning_some_networks_unreachable_message">Some networks currently are unreachable. Please try again later.</string>
<string name="warning_some_networks_unreachable_message">Swipe down to refresh or try again later.</string>
<string name="warning_some_networks_unreachable_title">Some networks are unreachable</string>
<string name="warning_testnet_card_message">This is a Testnet card. It cannot process transactions and should only be used for testing and development purposes.</string>
<string name="warning_testnet_card_title">For testing purposes only</string>

View file

@ -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 ->

View file

@ -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()
fun BigDecimal.stripZeroPlainString(): String = this.stripTrailingZeros().toPlainString()
/** Compares two [BigDecimal] numbers */
infix fun BigDecimal.isEqualTo(other: BigDecimal): Boolean = this.compareTo(other) == 0

View file

@ -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<Yield> {
private suspend fun getEnabledYieldsSync(): List<Yield> {
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

View file

@ -31,10 +31,12 @@ internal class YieldBalanceConverter : Converter<YieldBalanceWrapperDTO, YieldBa
validatorAddress = item.validatorAddress ?: item.validatorAddresses?.get(0),
date = item.date?.toDateTime(),
pendingActions = pendingActionConverter
.convertList(item.pendingActions),
.convertList(item.pendingActions)
.sortedBy { it.passthrough },
isPending = false,
)
},
}
.sortedWith(compareBy({ it.type }, { it.amount })),
integrationId = value.integrationId,
),
)

View file

@ -118,16 +118,16 @@ class YieldConverter(
val address = validatorDTO.address.asMandatory("address")
return Yield.Validator(
address = address,
status = convertValidatorStatus(validatorDTO.status),
name = validatorDTO.name,
status = convertValidatorStatus(validatorDTO.status.asMandatory("status")),
name = validatorDTO.name.asMandatory("name"),
image = validatorDTO.image,
website = validatorDTO.website,
apr = validatorDTO.apr,
commission = validatorDTO.commission,
stakedBalance = validatorDTO.stakedBalance,
votingPower = validatorDTO.votingPower,
preferred = validatorDTO.preferred,
isStrategicPartner = isStrategicPartner(validatorDTO.address, validatorDTO.name),
preferred = validatorDTO.preferred.asMandatory("preferred"),
isStrategicPartner = isStrategicPartner(validatorDTO.address, validatorDTO.name.asMandatory("name")),
)
}

View file

@ -83,8 +83,14 @@ internal object TokensDataModule {
@Provides
@Singleton
fun provideCurrencyChecksRepository(walletManagersFacade: WalletManagersFacade): CurrencyChecksRepository {
return DefaultCurrencyChecksRepository(walletManagersFacade = walletManagersFacade)
fun provideCurrencyChecksRepository(
walletManagersFacade: WalletManagersFacade,
coroutineDispatcherProvider: CoroutineDispatcherProvider,
): CurrencyChecksRepository {
return DefaultCurrencyChecksRepository(
walletManagersFacade = walletManagersFacade,
coroutineDispatchers = coroutineDispatcherProvider,
)
}
@Provides

View file

@ -2,6 +2,7 @@ package com.tangem.data.tokens.repository
import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
import com.tangem.blockchain.common.FeeResourceAmountProvider
import com.tangem.blockchain.common.MinimumSendAmountProvider
import com.tangem.blockchain.common.ReserveAmountProvider
import com.tangem.blockchain.common.UtxoAmountLimitProvider
import com.tangem.data.tokens.converters.UtxoConverter
@ -11,10 +12,13 @@ import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import java.math.BigDecimal
internal class DefaultCurrencyChecksRepository(
private val walletManagersFacade: WalletManagersFacade,
private val coroutineDispatchers: CoroutineDispatcherProvider,
) : CurrencyChecksRepository {
override suspend fun getExistentialDeposit(userWalletId: UserWalletId, network: Network): BigDecimal? {
val manager = walletManagersFacade.getOrCreateWalletManager(
@ -43,6 +47,17 @@ internal class DefaultCurrencyChecksRepository(
return if (manager is ReserveAmountProvider) manager.getReserveAmount() else null
}
override suspend fun getMinimumSendAmount(userWalletId: UserWalletId, network: Network): BigDecimal? {
return withContext(coroutineDispatchers.io) {
val manager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
network = network,
)
if (manager is MinimumSendAmountProvider) manager.getMinimumSendAmount() else null
}
}
override suspend fun getFeeResourceAmount(userWalletId: UserWalletId, network: Network): CurrencyAmount? {
val manager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,

View file

@ -141,5 +141,65 @@
"info":"swapTokensForExactETC",
"source":"https://etc.blockscout.com/tx/0xd79e03ca6b71529c94d576ff78c55b4371e2ef3ff8f9a47007995e5b9e77f879",
"name":"swap"
},
"0x6ab15071": {
"info": "stakePOL",
"source": "https://etherscan.io/tx/0x490d2f31faa3f2fdb1a9505888761d89aba1dd3dcfeb65f08651d094c1ee5021",
"name": "buyVoucher"
},
"0xe4457a8a": {
"info": "stakePOL",
"source": "https://etherscan.io/tx/0xb7b67ea0261fe2067e285eb942be6883ef34b3d313160f3ce122cad3e91f95f7",
"name": "buyVoucherPOL"
},
"0xc83ec04d": {
"info": "stakePOL",
"source": "https://etherscan.io/tx/0x0f7fa7154be5ae9bbe229963e3c0d3eea16b74311c50c4a35f3fcffc9519baed",
"name": "sellVoucher_new"
},
"0xe570b78b": {
"info": "stakePOL",
"source": "https://etherscan.io/tx/0xde1dda9ff3aed8b1d5feff53f72032cbd106a57c1ae2e7fa80016349e2e68341",
"name": "sellVoucher_newPOL"
},
"0xe97fddc2": {
"info": "stakePOL",
"source": "https://etherscan.io/tx/0x0478bc1229722c049eed737ef5a56baee6d7bde7ae21afad7a10e029700b378c",
"name": "unstakeClaimTokens_new"
},
"0x8759c234": {
"info": "stakePOL",
"source": "https://etherscan.io/tx/0x79075778b470358bef4cfc25a74a43e7ae60f1bc292e0bca3ebc163fee8bf0c7",
"name": "unstakeClaimTokens_newPOL"
},
"0xc7b8981c": {
"info": "stakePOL",
"source": "https://etherscan.io/tx/0xc2df9b41f7b3780a2dd06f0dfff72ecd5d3a5bde3f1d3f0eb926e14ff975e957",
"name": "withdrawRewards"
},
"0xe0db556b": {
"info": "stakePOL",
"source": "https://etherscan.io/tx/0x96ee64ad31a045982a4776336d852af3c25dc39b6d01bea987aefd460b816773",
"name": "withdrawRewardsPOL"
},
"0x982ef0a7": {
"info": "stakeBSC",
"source": "https://bscscan.com/tx/0xd5ab62ee525f3dc2821de44c31e278f2b49a0202f4e0279e6d0fced05a034595",
"name": "delegate"
},
"0x4d99dd16": {
"info": "stakeBSC",
"source": "https://bscscan.com/tx/0x866ba857e96051d7cea271723fccde4113c9909dccb534202a2a87ad4c56cb67",
"name": "undelegate"
},
"0x59491871": {
"info": "stakeBSC",
"source": "https://bscscan.com/tx/0x69615650be1e8a6d94258483f4e880fc6b4d4d9ae53d967c82d847691d91dd71",
"name": "redelegate"
},
"0xaad3ec96": {
"info": "stakeBSC",
"source": "https://bscscan.com/tx/0xfcbeac6e0a4ba5768d97d14b5115d08dda92defa8a8deee8bba34e2f0655c52b",
"name": "claim"
}
}

View file

@ -21,19 +21,19 @@ internal class SdkTransactionTypeConverter(
TxHistoryItem.TransactionType.Transfer
}
is TransactionType.TronStakingTransactionType.FreezeBalanceV2Contract -> {
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() })
}

View file

@ -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)

View file

@ -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"

View file

@ -16,7 +16,7 @@ class GetStakingAvailabilityUseCase(
private val stakingErrorResolver: StakingErrorResolver,
) {
operator fun invoke(
suspend operator fun invoke(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): Either<StakingError, StakingAvailability> {

View file

@ -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<BalanceItem>, action: StakingAction) {
private fun removeLockedBalance(balances: MutableList<BalanceItem>, action: StakingAction) {
val index = findBalanceIndex(balances, action, BalanceType.LOCKED)
if (index != -1) {
balances.removeAt(index)
}
}
private fun addStubStakedPendingTransaction(balances: MutableList<BalanceItem>, action: StakingAction) {
balances.add(
BalanceItem(
groupId = UUID.randomUUID().toString(),
@ -73,13 +93,51 @@ class InvalidatePendingTransactionsUseCase(
)
}
private fun modifyBalancesByStatus(balances: MutableList<BalanceItem>, action: StakingAction, type: BalanceType) {
val index = balances.indexOfFirst {
!it.isPending && it.amount == action.amount && it.type == type
}
private fun modifyBalancesByStatus(
balances: MutableList<BalanceItem>,
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<BalanceItem>, action: StakingAction, type: BalanceType): Int {
return balances.indexOfFirst {
!it.isPending && it.amount isEqualTo action.amount && it.type == type
}
}
private fun processPartialUnstake(balances: MutableList<BalanceItem>, 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<BalanceItem>, action: StakingAction): Pair<Int, BigDecimal> {
val index = balances.indexOfFirst {
!it.isPending && action.amount < it.amount &&
it.type == BalanceType.STAKED &&
it.validatorAddress == action.validatorAddress
}
return index to action.amount
}
}

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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<Throwable, BigDecimal?> = either {
val cryptoCurrency = cryptoCurrencyStatus.currency
currencyChecksRepository.getMinimumSendAmount(userWalletId, cryptoCurrency.network)
}
}

View file

@ -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?

View file

@ -112,7 +112,7 @@ class MockStakingRepository : StakingRepository {
isAvailable = false,
)
override fun getStakingAvailability(
override suspend fun getStakingAvailability(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): StakingAvailability = StakingAvailability.Unavailable

View file

@ -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<Either<GetFeeError, TransactionFee>> {
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<GetFeeError, TransactionFee> {
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()
}
}

View file

@ -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
}
}

View file

@ -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) {

View file

@ -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<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val minimumTransactionAmountProvider: Provider<EnterAmountBoundary?>,
) {
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,
)
}

View file

@ -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<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val minimumTransactionAmountProvider: Provider<EnterAmountBoundary?>,
) : Converter<AmountReduceByTransformer.ReduceByData, SendUiState> {
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),
)
}
}

View file

@ -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<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val minimumTransactionAmountProvider: Provider<EnterAmountBoundary?>,
) : Converter<BigDecimal, SendUiState> {
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),
)
}
}

View file

@ -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<NotificationUM>.addWarningNotifications(

View file

@ -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<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val minimumTransactionAmountProvider: Provider<EnterAmountBoundary?>,
) : Converter<String, SendUiState> {
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),
)
}
}

View file

@ -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<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val minimumTransactionAmountProvider: Provider<EnterAmountBoundary?>,
) : Converter<Unit, SendUiState> {
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),
)
}
}

View file

@ -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<AppCurrency> {
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

View file

@ -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

View file

@ -16,7 +16,7 @@ internal sealed class InnerYieldBalanceState {
val rewardsFiat: String,
val rewardBlockType: RewardBlockType,
val isActionable: Boolean,
val balance: ImmutableList<BalanceState>,
val balances: ImmutableList<BalanceState>,
) : InnerYieldBalanceState()
data object Empty : InnerYieldBalanceState()

View file

@ -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(

View file

@ -75,7 +75,7 @@ internal class StakingStateController @Inject constructor(
walletName = "",
cryptoCurrencyName = "",
cryptoCurrencySymbol = "",
cryptoCurrencyNetworkId = "",
cryptoCurrencyBlockchainId = "",
currentStep = StakingStep.InitialInfo,
initialInfoState = StakingStates.InitialInfoState.Empty(),
amountState = AmountState.Empty(),

View file

@ -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()

View file

@ -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,

View file

@ -53,7 +53,7 @@ internal class YieldBalancesConverter(
},
rewardBlockType = type,
isActionable = isActionable,
balance = balanceToShowItems.mapBalances(),
balances = balanceToShowItems.mapBalances(),
)
} else {
InnerYieldBalanceState.Empty

View file

@ -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()
}
}

View file

@ -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"),

View file

@ -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<CryptoCurrencyStatus>,
private val userWalletProvider: Provider<UserWallet>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val maxEnterAmountProvider: Provider<MaxEnterAmount>,
) : Transformer<StakingUiState> {
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 = "",

View file

@ -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<UserWallet>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val balancesToShowProvider: Provider<List<BalanceItem>>,
private val maxEnterAmountProvider: Provider<MaxEnterAmount>,
) : Transformer<StakingUiState> {
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 = "",

View file

@ -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<StakingUiState> {
@ -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(

View file

@ -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<StakingUiState> {
@ -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,

View file

@ -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<StakingUiState> {
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),
)
}
}

View file

@ -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<StakingUiState> {
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),
)
}
}

View file

@ -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<AmountState> {
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,

View file

@ -107,6 +107,7 @@ internal class AddStakingNotificationsTransformer(
notifications = this,
prevState = prevState,
sendingAmount = sendingAmount,
actionAmount = amountValue,
feeValue = feeValue,
)
}.toImmutableList()

View file

@ -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<NotificationUM>,
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<NotificationUM>.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)
}
}
}

View file

@ -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)

View file

@ -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,
)
}
}

View file

@ -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,
),
)

View file

@ -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,

View file

@ -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<StakingAction> = 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 ==

View file

@ -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<GetFeeError, TransactionFee>? {
): Either<GetFeeError, TransactionFee> {
return estimateFeeUseCase(
amount = amount,
userWallet = userWallet,
cryptoCurrency = cryptoCurrency,
).firstOrNull()
).mapFeeToForceSingleFeeIfNeeded(cryptoCurrency)
}
private fun Either<GetFeeError, TransactionFee>.mapFeeToForceSingleFeeIfNeeded(
cryptoCurrency: CryptoCurrency,
): Either<GetFeeError, TransactionFee> {
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

View file

@ -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(

View file

@ -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),
)
}
}
}
}

View file

@ -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<TokenDetailsState>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val stakingEntryInfoProvider: Provider<StakingEntryInfo?>,
private val pendingBalancesProvider: Provider<List<BalanceItem>>,
private val stakingAvailabilityProvider: Provider<StakingAvailability>,
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,

View file

@ -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<CryptoCurrency, TokenDetailsState> {
@ -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(),

View file

@ -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<StakingEntryInfo?>,
private val stakingAvailabilityProvider: Provider<StakingAvailability>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
private val pendingBalancesProvider: Provider<List<BalanceItem>>,
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,

View file

@ -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 ""
}

View file

@ -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<AppCurrency> = createSelectedAppCurrencyFlow()
private val stakingPendingBalances: List<BalanceItem> = 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,
)

View file

@ -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 ""
}

View file

@ -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(),

View file

@ -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 ^

View file

@ -416,4 +416,5 @@ private val excludedBlockchains = listOf(
Blockchain.Unknown,
Blockchain.Nexa,
Blockchain.NexaTestnet,
Blockchain.Xodex,
)