diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/components/PriceInputArea.kt b/app/src/main/java/one/mixin/android/ui/home/web3/components/PriceInputArea.kt index 9f0a9f3dec..a2b5ef473e 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/components/PriceInputArea.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/components/PriceInputArea.kt @@ -33,7 +33,8 @@ import one.mixin.android.api.response.web3.QuoteResult import one.mixin.android.api.response.web3.SwapToken import one.mixin.android.compose.theme.MixinAppTheme import one.mixin.android.ui.home.web3.trade.SwapViewModel -import one.mixin.android.ui.home.web3.trade.tradePriceInputMaxDecimalPlaces +import one.mixin.android.ui.home.web3.trade.limitTradeInputDecimalPlaces +import one.mixin.android.ui.home.web3.trade.tradeInputMaxDecimalPlaces import java.math.BigDecimal import java.math.RoundingMode @@ -60,6 +61,43 @@ fun PriceInputArea( // Loading state when fetching price from API var isPriceLoading by remember { mutableStateOf(false) } + val displayToken = if (isPriceInverted) fromToken else toToken + val displayMaxDecimalPlaces = displayToken.tradeInputMaxDecimalPlaces() + val standardMaxDecimalPlaces = toToken.tradeInputMaxDecimalPlaces() + + fun formatLimitedPrice(value: BigDecimal, maxDecimalPlaces: Int): String { + return value + .setScale(maxDecimalPlaces.coerceAtLeast(0), RoundingMode.DOWN) + .stripTrailingZeros() + .toPlainString() + } + + fun formatStandardPrice(value: BigDecimal): String = + formatLimitedPrice(value, standardMaxDecimalPlaces) + + fun formatDisplayPrice(standardPrice: BigDecimal): String { + return if (isPriceInverted && standardPrice > BigDecimal.ZERO) { + formatLimitedPrice( + BigDecimal.ONE.divide(standardPrice, displayMaxDecimalPlaces, RoundingMode.DOWN), + displayMaxDecimalPlaces, + ) + } else { + formatLimitedPrice(standardPrice, displayMaxDecimalPlaces) + } + } + + fun limitedStandardPriceFromDisplay(displayValue: String): String { + val inputPrice = displayValue.toBigDecimalOrNull() + return if (isPriceInverted && inputPrice != null && inputPrice > BigDecimal.ZERO) { + formatLimitedPrice( + BigDecimal.ONE.divide(inputPrice, standardMaxDecimalPlaces, RoundingMode.DOWN), + standardMaxDecimalPlaces, + ) + } else { + limitTradeInputDecimalPlaces(displayValue, standardMaxDecimalPlaces) + } + } + LaunchedEffect(fromToken, toToken) { val isFromUsd = fromToken?.assetId?.let { id -> Constants.AssetId.usdtAssets.containsKey(id) || Constants.AssetId.usdcAssets.containsKey(id) @@ -73,15 +111,9 @@ fun PriceInputArea( LaunchedEffect(priceMultiplier) { if (priceMultiplier != null && marketPrice != null && marketPrice!! > BigDecimal.ZERO) { val newPrice = marketPrice!!.multiply(BigDecimal(priceMultiplier.toString())) - .setScale(8, RoundingMode.HALF_UP) - val priceString = newPrice.stripTrailingZeros().toPlainString() + val priceString = formatStandardPrice(newPrice) onStandardPriceChanged(priceString) - - displayPrice = if (isPriceInverted && newPrice > BigDecimal.ZERO) { - BigDecimal.ONE.divide(newPrice, 8, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString() - } else { - priceString - } + displayPrice = formatDisplayPrice(newPrice) } } @@ -100,14 +132,9 @@ fun PriceInputArea( if (fromP != null && toP != null && toP > BigDecimal.ZERO) { val price = fromP.multiply(BigDecimal(0.99)).divide(toP, 8, RoundingMode.HALF_UP) marketPrice = price - val priceString = price.stripTrailingZeros().toPlainString() + val priceString = formatStandardPrice(price) onStandardPriceChanged(priceString) - - displayPrice = if (isPriceInverted && price > BigDecimal.ZERO) { - BigDecimal.ONE.divide(price, 8, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString() - } else { - priceString - } + displayPrice = formatDisplayPrice(price) fromMarket = viewModel.checkMarketById(fromT.assetId, true) toMarket = viewModel.checkMarketById(toT.assetId, true) @@ -117,14 +144,9 @@ fun PriceInputArea( val updatedPrice = fromP.multiply(BigDecimal(0.99)).divide(toP, 8, RoundingMode.HALF_UP) if (price != updatedPrice) { marketPrice = updatedPrice - val updatedPriceString = updatedPrice.stripTrailingZeros().toPlainString() + val updatedPriceString = formatStandardPrice(updatedPrice) onStandardPriceChanged(updatedPriceString) - - displayPrice = if (isPriceInverted && updatedPrice > BigDecimal.ZERO) { - BigDecimal.ONE.divide(updatedPrice, 8, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString() - } else { - updatedPriceString - } + displayPrice = formatDisplayPrice(updatedPrice) } } } else { @@ -134,16 +156,11 @@ fun PriceInputArea( .onSuccess { q -> val rate = runCatching { parseRateFromQuote(q) }.getOrNull() ?: BigDecimal.ZERO if (rate > BigDecimal.ZERO) { - val price = rate.setScale(8, RoundingMode.HALF_UP) + val price = rate marketPrice = price - val priceString = price.stripTrailingZeros().toPlainString() + val priceString = formatStandardPrice(price) onStandardPriceChanged(priceString) - - displayPrice = if (isPriceInverted && price > BigDecimal.ZERO) { - BigDecimal.ONE.divide(price, 8, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString() - } else { - priceString - } + displayPrice = formatDisplayPrice(price) } else { displayPrice = "" onStandardPriceChanged("") @@ -176,16 +193,11 @@ fun PriceInputArea( title = stringResource(id = R.string.limit_price, priceDisplayState.displayChainName), readOnly = false, selectClick = null, - maxDecimalPlaces = tradePriceInputMaxDecimalPlaces(), + maxDecimalPlaces = displayMaxDecimalPlaces, onInputChanged = { userInput -> - displayPrice = userInput - val inputPrice = userInput.toBigDecimalOrNull() - val newStandardPrice = if (isPriceInverted && inputPrice != null && inputPrice > BigDecimal.ZERO) { - BigDecimal.ONE.divide(inputPrice, 8, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString() - } else { - userInput - } - onStandardPriceChanged(newStandardPrice) + val limitedDisplayPrice = limitTradeInputDecimalPlaces(userInput, displayMaxDecimalPlaces) + displayPrice = limitedDisplayPrice + onStandardPriceChanged(limitedStandardPriceFromDisplay(limitedDisplayPrice)) }, inlineEndCompose = if (priceDisplayState.isPriceLoading) { { diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/InputTextField.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/InputTextField.kt index 4693e75eea..ae4ecedd1f 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/InputTextField.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/InputTextField.kt @@ -82,6 +82,7 @@ internal fun isTradeInputDecimalAllowed( ): Boolean { maxDecimalPlaces ?: return true val decimalIndex = value.indexOf('.') + if (maxDecimalPlaces == 0) return decimalIndex < 0 return decimalIndex < 0 || value.length - decimalIndex - 1 <= maxDecimalPlaces } @@ -92,6 +93,7 @@ internal fun limitTradeInputDecimalPlaces( maxDecimalPlaces ?: return value val decimalIndex = value.indexOf('.') if (decimalIndex < 0) return value + if (maxDecimalPlaces == 0) return value.substring(0, decimalIndex) val endIndex = (decimalIndex + 1 + maxDecimalPlaces).coerceAtMost(value.length) return value.substring(0, endIndex) } diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/LimitOrderContent.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/LimitOrderContent.kt index 6393ece5ed..05b73c17f6 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/LimitOrderContent.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/LimitOrderContent.kt @@ -177,10 +177,16 @@ fun LimitOrderContent( } var outputText by remember { mutableStateOf("") } - LaunchedEffect(lastOrderTime, fromMaxDecimalPlaces) { + LaunchedEffect(lastOrderTime) { inputText = limitTradeInputDecimalPlaces(initialAmount ?: "", fromMaxDecimalPlaces) outputText = "" } + LaunchedEffect(fromMaxDecimalPlaces) { + inputText = limitTradeInputDecimalPlaces(inputText, fromMaxDecimalPlaces) + } + LaunchedEffect(toMaxDecimalPlaces) { + outputText = limitTradeInputDecimalPlaces(outputText, toMaxDecimalPlaces) + } var isButtonEnabled by remember { mutableStateOf(true) } var isSubmitting by remember { mutableStateOf(false) } @@ -282,9 +288,13 @@ fun LimitOrderContent( val oldPrice = limitPriceText.toBigDecimalOrNull() if (oldPrice != null && oldPrice > BigDecimal.ZERO) { - limitPriceText = BigDecimal.ONE.divide( - oldPrice, 8, RoundingMode.HALF_UP - ).stripTrailingZeros().toPlainString() + val nextPriceMaxDecimalPlaces = fromToken.tradeInputMaxDecimalPlaces() + limitPriceText = limitTradeInputDecimalPlaces( + BigDecimal.ONE.divide(oldPrice, nextPriceMaxDecimalPlaces, RoundingMode.DOWN) + .stripTrailingZeros() + .toPlainString(), + nextPriceMaxDecimalPlaces, + ) } fromToken?.let { f -> diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/SwapContent.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/SwapContent.kt index e0eb2e806b..4a21c37c03 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/SwapContent.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/SwapContent.kt @@ -137,9 +137,12 @@ fun SwapContent( var inputText by remember { mutableStateOf(limitTradeInputDecimalPlaces(initialAmount ?: "", fromMaxDecimalPlaces)) } - LaunchedEffect(lastOrderTime, fromMaxDecimalPlaces) { + LaunchedEffect(lastOrderTime) { inputText = limitTradeInputDecimalPlaces(initialAmount ?: "", fromMaxDecimalPlaces) } + LaunchedEffect(fromMaxDecimalPlaces) { + inputText = limitTradeInputDecimalPlaces(inputText, fromMaxDecimalPlaces) + } val shouldRefreshQuote = remember { MutableStateFlow(inputText) } var isButtonEnabled by remember { mutableStateOf(true) } diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionPage.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionPage.kt index 3c7f358c42..02e0dd5927 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionPage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionPage.kt @@ -71,7 +71,9 @@ import one.mixin.android.ui.home.web3.components.PageScaffold import one.mixin.android.ui.home.web3.trade.InputContent import one.mixin.android.ui.home.web3.trade.KeyboardAwareBox import one.mixin.android.ui.home.web3.trade.SwapActivity +import one.mixin.android.ui.home.web3.trade.TRADE_INPUT_MAX_DECIMAL_PLACES import one.mixin.android.ui.home.web3.trade.TradeFragment +import one.mixin.android.ui.home.web3.trade.limitTradeInputDecimalPlaces import one.mixin.android.ui.wallet.AddFeeBottomSheetDialogFragment import one.mixin.android.ui.wallet.WalletActivity import one.mixin.android.ui.wallet.alert.components.cardBackground @@ -230,9 +232,13 @@ fun OpenPositionPage( isLiquidationLoading = true delay(200L) while (true) { + val normalizedAmount = amount + .stripTrailingZeros() + .toPlainString() + .let { limitTradeInputDecimalPlaces(it, TRADE_INPUT_MAX_DECIMAL_PLACES) } val result = viewModel.estimateLiquidationPrice( marketId = currentMarket.marketId, - amount = amount.stripTrailingZeros().toPlainString(), + amount = normalizedAmount, side = if (isLong) "long" else "short", leverage = leverage.toInt(), ) @@ -413,6 +419,7 @@ fun OpenPositionPage( }, onInputChanged = { usdtAmount = it }, tokenIconSize = 25.dp, + maxDecimalPlaces = TRADE_INPUT_MAX_DECIMAL_PLACES, ) Row( @@ -435,7 +442,7 @@ fun OpenPositionPage( ), modifier = Modifier.clickable { AnalyticsTracker.trackPerpsAmountInputBalance() - usdtAmount = currentToken?.balance ?: "0" + usdtAmount = limitTradeInputDecimalPlaces(currentToken?.balance ?: "0", TRADE_INPUT_MAX_DECIMAL_PLACES) } ) if (showAddAction) { @@ -758,11 +765,15 @@ fun OpenPositionPage( return@launch } + val normalizedAmount = amount + .stripTrailingZeros() + .toPlainString() + .let { limitTradeInputDecimalPlaces(it, TRADE_INPUT_MAX_DECIMAL_PLACES) } viewModel.openPerpsOrder( assetId = token.assetId, marketId = m.marketId, side = if (isLong) "long" else "short", - amount = amount.stripTrailingZeros().toPlainString(), + amount = normalizedAmount, leverage = leverage.toInt(), walletId = walletId, // Null means "leave TP/SL unset" when creating a new position. @@ -833,6 +844,7 @@ fun OpenPositionPage( .multiply(percent) .stripTrailingZeros() .toPlainString() + .let { limitTradeInputDecimalPlaces(it, TRADE_INPUT_MAX_DECIMAL_PLACES) } } else { usdtAmount = "" } diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsAddBottomSheetDialogFragment.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsAddBottomSheetDialogFragment.kt index 3145719aec..1ecdddde00 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsAddBottomSheetDialogFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsAddBottomSheetDialogFragment.kt @@ -82,7 +82,9 @@ import one.mixin.android.ui.home.web3.components.InputAction import one.mixin.android.ui.home.web3.trade.InputContent import one.mixin.android.ui.home.web3.trade.KeyboardAwareBox import one.mixin.android.ui.home.web3.trade.SwapActivity +import one.mixin.android.ui.home.web3.trade.TRADE_INPUT_MAX_DECIMAL_PLACES import one.mixin.android.ui.home.web3.trade.TradeFragment +import one.mixin.android.ui.home.web3.trade.limitTradeInputDecimalPlaces import one.mixin.android.ui.home.web3.trade.perps.formatPerpsPrice import one.mixin.android.ui.home.web3.trade.perps.formatPerpsQuantity import one.mixin.android.ui.home.web3.trade.perps.formatPerpsUsdDecimal @@ -301,8 +303,12 @@ private fun PerpsAddContent( isLiquidationLoading = true delay(200L) while (true) { + val normalizedAmount = addMargin + .stripTrailingZeros() + .toPlainString() + .let { limitTradeInputDecimalPlaces(it, TRADE_INPUT_MAX_DECIMAL_PLACES) } val result = viewModel.estimateLiquidationPrice( - amount = addMargin.stripTrailingZeros().toPlainString(), + amount = normalizedAmount, positionId = position.positionId, ) if (result != null) { @@ -472,6 +478,7 @@ private fun PerpsAddContent( onInputChanged = { amount = it }, tokenIconSize = 25.dp, autoFocus = true, + maxDecimalPlaces = TRADE_INPUT_MAX_DECIMAL_PLACES, ) Row( @@ -493,7 +500,7 @@ private fun PerpsAddContent( textAlign = TextAlign.Start, ), modifier = Modifier.clickable { - amount = selectedToken?.balance ?: "0" + amount = limitTradeInputDecimalPlaces(selectedToken?.balance ?: "0", TRADE_INPUT_MAX_DECIMAL_PLACES) }, ) if (insufficientBalance || tokenBalance <= BigDecimal.ZERO) { @@ -606,11 +613,12 @@ private fun PerpsAddContent( onAdd = { val token = selectedToken ?: return@PerpsAddActionFooter val normalizedAmount = amount.toBigDecimalOrNull() - ?.stripTrailingZeros() - ?.toPlainString() - ?: return@PerpsAddActionFooter - onAdd(token, normalizedAmount, displayLiquidationPrice) - }, + ?.stripTrailingZeros() + ?.toPlainString() + ?.let { limitTradeInputDecimalPlaces(it, TRADE_INPUT_MAX_DECIMAL_PLACES) } + ?: return@PerpsAddActionFooter + onAdd(token, normalizedAmount, displayLiquidationPrice) + }, ) } } @@ -618,7 +626,10 @@ private fun PerpsAddContent( floating = { fun applyBalancePercent(percent: BigDecimal) { amount = if (tokenBalance > BigDecimal.ZERO) { - tokenBalance.multiply(percent).stripTrailingZeros().toPlainString() + tokenBalance.multiply(percent) + .stripTrailingZeros() + .toPlainString() + .let { limitTradeInputDecimalPlaces(it, TRADE_INPUT_MAX_DECIMAL_PLACES) } } else { "" } @@ -642,6 +653,7 @@ private fun PerpsAddContent( val normalizedAmount = amount.toBigDecimalOrNull() ?.stripTrailingZeros() ?.toPlainString() + ?.let { limitTradeInputDecimalPlaces(it, TRADE_INPUT_MAX_DECIMAL_PLACES) } ?: return@PerpsAddActionFooter onAdd(token, normalizedAmount, displayLiquidationPrice) }, diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsTpSlBottomSheetDialogFragment.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsTpSlBottomSheetDialogFragment.kt index 2cbc184dfa..be1d239ef8 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsTpSlBottomSheetDialogFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsTpSlBottomSheetDialogFragment.kt @@ -301,6 +301,9 @@ private fun PerpsTpSlContent( preferences.getString(PREF_TPSL_INPUT_TYPE, null) ?.let { stored -> InputType.values().firstOrNull { it.name == stored } } } + val normalizedInitialPrice = remember(initialPrice, safePriceScale) { + normalizePriceInput(initialPrice, safePriceScale) + } val defaultInputType = remember(initialPrice, storedInputType) { storedInputType ?: if (initialPrice.isBlank()) InputType.PNL else InputType.PRICE } @@ -308,14 +311,14 @@ private fun PerpsTpSlContent( mutableStateOf(defaultInputType) } var priceFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) { - mutableStateOf(textFieldValueAtEnd(initialPrice)) + mutableStateOf(textFieldValueAtEnd(normalizedInitialPrice)) } var percentFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf( textFieldValueAtEnd( normalizePercentInput( derivePercentMagnitudeInput( - priceInput = initialPrice, + priceInput = normalizedInitialPrice, percentBasePrice = percentBasePrice, leverage = leverageValue, isLong = isLong,