Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-11 12:42:36 +03:00
parent 25c73300fe
commit 0ddd171bb2
7 changed files with 155 additions and 85 deletions

View file

@ -277,11 +277,9 @@ internal object TokensDomainModule {
@Singleton
fun provideGetMinimumTransactionAmountSyncUseCase(
currencyChecksRepository: CurrencyChecksRepository,
dispatchers: CoroutineDispatcherProvider,
): GetMinimumTransactionAmountSyncUseCase {
return GetMinimumTransactionAmountSyncUseCase(
currencyChecksRepository = currencyChecksRepository,
dispatchers = dispatchers,
)
}

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

@ -95,8 +95,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

@ -12,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(
@ -45,12 +48,14 @@ internal class DefaultCurrencyChecksRepository(
}
override suspend fun getMinimumSendAmount(userWalletId: UserWalletId, network: Network): BigDecimal? {
val manager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
network = network,
)
return withContext(coroutineDispatchers.io) {
val manager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
network = network,
)
return if (manager is MinimumSendAmountProvider) manager.getMinimumSendAmount() else null
if (manager is MinimumSendAmountProvider) manager.getMinimumSendAmount() else null
}
}
override suspend fun getFeeResourceAmount(userWalletId: UserWalletId, network: Network): CurrencyAmount? {

View file

@ -5,22 +5,17 @@ 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 com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import java.math.BigDecimal
class GetMinimumTransactionAmountSyncUseCase(
private val currencyChecksRepository: CurrencyChecksRepository,
private val dispatchers: CoroutineDispatcherProvider,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): Either<Throwable, BigDecimal?> = withContext(dispatchers.io) {
either {
val cryptoCurrency = cryptoCurrencyStatus.currency
currencyChecksRepository.getMinimumSendAmount(userWalletId, cryptoCurrency.network)
}
): Either<Throwable, BigDecimal?> = either {
val cryptoCurrency = cryptoCurrencyStatus.currency
currencyChecksRepository.getMinimumSendAmount(userWalletId, cryptoCurrency.network)
}
}

View file

@ -172,10 +172,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,
@ -252,10 +257,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,
@ -863,14 +874,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,
),
)
}

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
@ -83,6 +84,7 @@ internal class SwapViewModel @Inject constructor(
private val getCardInfoUseCase: GetCardInfoUseCase,
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase,
swapInteractorFactory: SwapInteractor.Factory,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver {
@ -842,60 +844,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),
)
}
}
}
}