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 @@ -72,7 +72,7 @@ internal fun tradeInputMaxDecimalPlaces(

internal fun SwapToken?.tradeInputMaxDecimalPlaces(): Int {
return this?.let { token ->
tradeInputMaxDecimalPlaces(token.walletId != null, token.decimals)
tradeInputMaxDecimalPlaces(token.walletId != null && !token.isWeb3, token.decimals)
} ?: TRADE_INPUT_MAX_DECIMAL_PLACES
}

Expand All @@ -81,21 +81,41 @@ internal fun isTradeInputDecimalAllowed(
maxDecimalPlaces: Int? = TRADE_INPUT_MAX_DECIMAL_PLACES,
): Boolean {
maxDecimalPlaces ?: return true
if (maxDecimalPlaces < 0) return true
val decimalIndex = value.indexOf('.')
return decimalIndex < 0 || value.length - decimalIndex - 1 <= maxDecimalPlaces
if (decimalIndex < 0) return true
if (maxDecimalPlaces == 0) return false
return value.length - decimalIndex - 1 <= maxDecimalPlaces
}

internal fun limitTradeInputDecimalPlaces(
value: String,
maxDecimalPlaces: Int? = TRADE_INPUT_MAX_DECIMAL_PLACES,
): String {
maxDecimalPlaces ?: return value
if (maxDecimalPlaces < 0) 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)
}

internal fun limitTradeInputTextFieldValue(
value: TextFieldValue,
maxDecimalPlaces: Int? = TRADE_INPUT_MAX_DECIMAL_PLACES,
): TextFieldValue {
val limitedText = limitTradeInputDecimalPlaces(value.text, maxDecimalPlaces)
if (limitedText == value.text) return value
return value.copy(
text = limitedText,
selection = TextRange(
value.selection.start.coerceAtMost(limitedText.length),
value.selection.end.coerceAtMost(limitedText.length),
),
)
}

@SuppressLint("UnrememberedMutableState")
@Composable
fun InputContent(
Expand Down Expand Up @@ -147,21 +167,24 @@ fun InputContent(
}
val interactionSource = remember { MutableInteractionSource() }
var textFieldValue by remember {
val limitedText = limitTradeInputDecimalPlaces(text, maxDecimalPlaces)
mutableStateOf(
TextFieldValue(
text = text,
selection = TextRange(text.length)
text = limitedText,
selection = TextRange(limitedText.length)
)
)
}

LaunchedEffect(text) {
if (textFieldValue.text != text) {
LaunchedEffect(text, maxDecimalPlaces) {
val limitedText = limitTradeInputDecimalPlaces(text, maxDecimalPlaces)
if (textFieldValue.text != limitedText) {
textFieldValue = TextFieldValue(
text = text,
selection = TextRange(text.length)
text = limitedText,
selection = TextRange(limitedText.length)
)
}
if (text != limitedText) onInputChanged?.invoke(limitedText)
}

Column(modifier = Modifier.fillMaxWidth()) {
Expand All @@ -178,16 +201,14 @@ fun InputContent(
BasicTextField(
value = textFieldValue,
onValueChange = {
if (!isTradeInputDecimalAllowed(it.text, maxDecimalPlaces)) {
return@BasicTextField
}
textFieldValue = it
val limitedValue = limitTradeInputTextFieldValue(it, maxDecimalPlaces)
textFieldValue = limitedValue
try {
if (it.text.isBlank()) BigDecimal.ZERO else BigDecimal(it.text)
if (limitedValue.text.isBlank()) BigDecimal.ZERO else BigDecimal(limitedValue.text)
} catch (e: Exception) {
return@BasicTextField
}
onInputChanged?.invoke(it.text)
onInputChanged?.invoke(limitedValue.text)
},
maxLines = 1,
modifier = Modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ 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.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 @@ -144,6 +145,7 @@ fun OpenPositionPage(
var currentToken by remember { mutableStateOf<TokenItem?>(selectedToken) }
var availableTokens by remember { mutableStateOf<List<TokenItem>>(emptyList()) }
var usdtAmount by remember { mutableStateOf("") }
val amountMaxDecimalPlaces = PERPS_AMOUNT_MAX_DECIMAL_PLACES
var takeProfitPrice by remember { mutableStateOf("") }
var stopLossPrice by remember { mutableStateOf("") }
var remoteLiquidationPrice by remember { mutableStateOf<String?>(null) }
Expand All @@ -160,6 +162,10 @@ fun OpenPositionPage(
}
var leverage by remember(marketId) { mutableFloatStateOf(savedLeverage.toFloat()) }

LaunchedEffect(currentToken?.assetId, amountMaxDecimalPlaces) {
usdtAmount = limitTradeInputDecimalPlaces(usdtAmount, amountMaxDecimalPlaces)
}

LaunchedEffect(marketId) {
while (true) {
viewModel.loadMarketDetail(
Expand Down Expand Up @@ -413,6 +419,7 @@ fun OpenPositionPage(
},
onInputChanged = { usdtAmount = it },
tokenIconSize = 25.dp,
maxDecimalPlaces = amountMaxDecimalPlaces,
)

Row(
Expand All @@ -435,7 +442,7 @@ fun OpenPositionPage(
),
modifier = Modifier.clickable {
AnalyticsTracker.trackPerpsAmountInputBalance()
usdtAmount = currentToken?.balance ?: "0"
usdtAmount = limitTradeInputDecimalPlaces(currentToken?.balance ?: "0", amountMaxDecimalPlaces)
}
)
if (showAddAction) {
Expand Down Expand Up @@ -829,10 +836,13 @@ fun OpenPositionPage(
floating = {
fun applyBalancePercent(percent: BigDecimal) {
if (tokenBalance > BigDecimal.ZERO) {
usdtAmount = tokenBalance
.multiply(percent)
.stripTrailingZeros()
.toPlainString()
usdtAmount = limitTradeInputDecimalPlaces(
tokenBalance
.multiply(percent)
.stripTrailingZeros()
.toPlainString(),
amountMaxDecimalPlaces,
)
} else {
usdtAmount = ""
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ 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.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 @@ -267,6 +268,7 @@ private fun PerpsAddContent(
val focusManager = LocalFocusManager.current
val viewModel = hiltViewModel<PerpetualViewModel>()
var amount by remember(position.positionId) { mutableStateOf("") }
val amountMaxDecimalPlaces = PERPS_AMOUNT_MAX_DECIMAL_PLACES
var remoteLiquidationPrice by remember(position.positionId) { mutableStateOf<String?>(null) }
var isLiquidationLoading by remember(position.positionId) { mutableStateOf(false) }
var liquidationJob by remember(position.positionId) { mutableStateOf<Job?>(null) }
Expand All @@ -288,6 +290,10 @@ private fun PerpsAddContent(
.ifBlank { position.entryPrice }
val priceScale = market?.priceScale ?: position.priceScale

LaunchedEffect(selectedToken?.assetId, amountMaxDecimalPlaces) {
amount = limitTradeInputDecimalPlaces(amount, amountMaxDecimalPlaces)
}

LaunchedEffect(amount, belowMinimumMargin, aboveMaximumMargin) {
val addMargin = amount.toBigDecimalOrNull()
if (addMargin == null || addMargin <= BigDecimal.ZERO || belowMinimumMargin || aboveMaximumMargin) {
Expand Down Expand Up @@ -472,6 +478,7 @@ private fun PerpsAddContent(
onInputChanged = { amount = it },
tokenIconSize = 25.dp,
autoFocus = true,
maxDecimalPlaces = amountMaxDecimalPlaces,
)

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", amountMaxDecimalPlaces)
},
)
if (insufficientBalance || tokenBalance <= BigDecimal.ZERO) {
Expand Down Expand Up @@ -618,7 +625,10 @@ private fun PerpsAddContent(
floating = {
fun applyBalancePercent(percent: BigDecimal) {
amount = if (tokenBalance > BigDecimal.ZERO) {
tokenBalance.multiply(percent).stripTrailingZeros().toPlainString()
limitTradeInputDecimalPlaces(
tokenBalance.multiply(percent).stripTrailingZeros().toPlainString(),
amountMaxDecimalPlaces,
)
} else {
""
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import java.math.BigDecimal
import java.math.RoundingMode

const val PERPS_USD_SYMBOL = "\$"
internal const val PERPS_AMOUNT_MAX_DECIMAL_PLACES = 2

fun PerpsMarket.changePercent(): BigDecimal {
return try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,15 +307,18 @@ private fun PerpsTpSlContent(
var inputType by rememberSaveable(initialPrice, mode.name) {
mutableStateOf(defaultInputType)
}
val initialPriceInput = remember(initialPrice, safePriceScale) {
normalizePriceInput(initialPrice, safePriceScale)
}
var priceFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(textFieldValueAtEnd(initialPrice))
mutableStateOf(textFieldValueAtEnd(initialPriceInput))
}
var percentFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(
textFieldValueAtEnd(
normalizePercentInput(
derivePercentMagnitudeInput(
priceInput = initialPrice,
priceInput = initialPriceInput,
percentBasePrice = percentBasePrice,
leverage = leverageValue,
isLong = isLong,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ class SwapTokenListBottomSheetDialogFragment : MixinBottomSheetDialogFragment()
successBlock = { resp ->
return@handleMixinResponse resp.data?.map {
if (!inMixin) {
it.copy(walletId = Web3Signer.currentWalletId)
it.copy(walletId = Web3Signer.currentWalletId, isWeb3 = true)
} else {
it
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package one.mixin.android.ui.home.web3.trade

import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import one.mixin.android.api.response.web3.SwapChain
import one.mixin.android.api.response.web3.SwapToken
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
Expand All @@ -14,6 +18,28 @@ class TradeInputTest {
assertEquals(8, tradeInputMaxDecimalPlaces(isCommonWallet = true, precision = -1))
}

@Test
fun web3TradeAmountInputUsesSharedDecimalLimitEvenWithWalletId() {
val token = token(
walletId = "web3-wallet",
decimals = 18,
isWeb3 = true,
)

assertEquals(8, token.tradeInputMaxDecimalPlaces())
}

@Test
fun commonWalletTradeAmountInputUsesTokenDecimalPlaces() {
val token = token(
walletId = "common-wallet",
decimals = 6,
isWeb3 = false,
)

assertEquals(6, token.tradeInputMaxDecimalPlaces())
}

@Test
fun tradePriceInputAllowsAtMostEightDecimalPlaces() {
assertEquals(8, tradePriceInputMaxDecimalPlaces())
Expand All @@ -22,7 +48,7 @@ class TradeInputTest {
}

@Test
fun tradeAmountInputAllowsAtMostEightDecimalPlaces() {
fun tradeAmountInputAllowsConfiguredDecimalPlaces() {
assertTrue(isTradeInputDecimalAllowed(""))
assertTrue(isTradeInputDecimalAllowed("12"))
assertTrue(isTradeInputDecimalAllowed("12."))
Expand All @@ -31,21 +57,57 @@ class TradeInputTest {

assertFalse(isTradeInputDecimalAllowed("12.123456789"))
assertFalse(isTradeInputDecimalAllowed("0.000000001"))
assertFalse(isTradeInputDecimalAllowed("1.234", maxDecimalPlaces = 2))
assertFalse(isTradeInputDecimalAllowed("5.", maxDecimalPlaces = 0))
assertFalse(isTradeInputDecimalAllowed("5.0", maxDecimalPlaces = 0))

assertTrue(isTradeInputDecimalAllowed("12.123456789", maxDecimalPlaces = null))
}

@Test
fun tradeAmountInputLimitsProgrammaticValuesToEightDecimalPlaces() {
fun tradeAmountInputLimitsProgrammaticValuesToConfiguredDecimalPlaces() {
assertEquals("", limitTradeInputDecimalPlaces(""))
assertEquals("12", limitTradeInputDecimalPlaces("12"))
assertEquals("12.", limitTradeInputDecimalPlaces("12."))
assertEquals("12.12345678", limitTradeInputDecimalPlaces("12.12345678"))
assertEquals("12.12345678", limitTradeInputDecimalPlaces("12.123456789"))
assertEquals("0.00000000", limitTradeInputDecimalPlaces("0.000000001"))
assertEquals("1.23", limitTradeInputDecimalPlaces("1.234", maxDecimalPlaces = 2))
assertEquals("5", limitTradeInputDecimalPlaces("5.", maxDecimalPlaces = 0))
assertEquals("5", limitTradeInputDecimalPlaces("5.123", maxDecimalPlaces = 0))
assertEquals(
"12.123456789",
limitTradeInputDecimalPlaces("12.123456789", maxDecimalPlaces = null)
)
}

@Test
fun tradeInputTextFieldValueLimitsPastedDecimals() {
val value = limitTradeInputTextFieldValue(
value = TextFieldValue(
text = "12.123456789",
selection = TextRange(12),
),
maxDecimalPlaces = 8,
)

assertEquals("12.12345678", value.text)
assertEquals(TextRange(11), value.selection)
}

private fun token(
walletId: String?,
decimals: Int,
isWeb3: Boolean,
) = SwapToken(
walletId = walletId,
address = "",
assetId = "asset-id",
decimals = decimals,
name = "Token",
symbol = "TKN",
icon = "",
chain = SwapChain("", "", "", ""),
isWeb3 = isWeb3,
)
}