Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand All @@ -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)
}
}

Expand All @@ -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)
Expand All @@ -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 {
Expand All @@ -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("")
Expand Down Expand Up @@ -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) {
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand Down Expand Up @@ -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 ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(),
)
Expand Down Expand Up @@ -413,6 +419,7 @@ fun OpenPositionPage(
},
onInputChanged = { usdtAmount = it },
tokenIconSize = 25.dp,
maxDecimalPlaces = TRADE_INPUT_MAX_DECIMAL_PLACES,
)

Row(
Expand All @@ -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) {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -833,6 +844,7 @@ fun OpenPositionPage(
.multiply(percent)
.stripTrailingZeros()
.toPlainString()
.let { limitTradeInputDecimalPlaces(it, TRADE_INPUT_MAX_DECIMAL_PLACES) }
} else {
usdtAmount = ""
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -472,6 +478,7 @@ private fun PerpsAddContent(
onInputChanged = { amount = it },
tokenIconSize = 25.dp,
autoFocus = true,
maxDecimalPlaces = TRADE_INPUT_MAX_DECIMAL_PLACES,
)

Row(
Expand All @@ -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) {
Expand Down Expand Up @@ -606,19 +613,23 @@ 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)
},
)
}
}
},
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 {
""
}
Expand All @@ -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)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,21 +301,24 @@ 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
}
var inputType by rememberSaveable(initialPrice, mode.name) {
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,
Expand Down