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
@@ -1,11 +1,20 @@
package one.mixin.android.ui.home.web3.trade.perps

import com.google.gson.JsonElement
import kotlinx.coroutines.delay
import one.mixin.android.util.ErrorHandler
import java.math.BigDecimal

internal data class LiquidationPriceLimit(
val maxAmount: String?,
val maxLeverage: Int?,
)

internal sealed interface LiquidationPriceResult {
data class Success(val price: String) : LiquidationPriceResult

data class LimitExceeded(val limit: LiquidationPriceLimit) : LiquidationPriceResult

data object Retry : LiquidationPriceResult

data object Failure : LiquidationPriceResult
Expand All @@ -14,15 +23,36 @@ internal sealed interface LiquidationPriceResult {
internal fun liquidationPriceResult(
price: String?,
errorCode: Int?,
limit: LiquidationPriceLimit = LiquidationPriceLimit(null, null),
): LiquidationPriceResult {
val validPrice = price?.takeIf { it.isNotBlank() }
return when {
validPrice != null -> LiquidationPriceResult.Success(validPrice)
errorCode == ErrorHandler.PERPS_POSITION_SIZE_EXCEEDS_LEVERAGE_LIMIT -> {
LiquidationPriceResult.LimitExceeded(limit)
}
errorCode == 500 -> LiquidationPriceResult.Retry
else -> LiquidationPriceResult.Failure
}
}

internal fun parseLiquidationPriceLimit(extra: JsonElement?): LiquidationPriceLimit {
val extraObject = extra?.takeIf { it.isJsonObject }?.asJsonObject
val maxAmount = extraObject
?.get("max_amount")
?.takeIf { !it.isJsonNull }
?.runCatching { asString }
?.getOrNull()
?.takeIf { it.isNotBlank() }
val maxLeverage = extraObject
?.get("max_leverage")
?.takeIf { !it.isJsonNull }
?.runCatching { asString.toIntOrNull() }
?.getOrNull()
?.takeIf { it > 0 }
return LiquidationPriceLimit(maxAmount, maxLeverage)
}

internal fun shouldRequestLiquidationPrice(
amount: BigDecimal?,
minimumAmount: BigDecimal,
Expand All @@ -34,11 +64,16 @@ internal fun shouldRequestLiquidationPrice(

internal suspend fun requestLiquidationPrice(
retryDelayMillis: Long = 1000L,
onLimitExceeded: (LiquidationPriceLimit) -> Unit = {},
request: suspend () -> LiquidationPriceResult,
): String? {
while (true) {
when (val result = request()) {
LiquidationPriceResult.Failure -> return null
is LiquidationPriceResult.LimitExceeded -> {
onLimitExceeded(result.limit)
return null
}
LiquidationPriceResult.Retry -> delay(retryDelayMillis)
is LiquidationPriceResult.Success -> return result.price
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ fun OpenPositionPage(
var takeProfitPrice by remember { mutableStateOf("") }
var stopLossPrice by remember { mutableStateOf("") }
var remoteLiquidationPrice by remember { mutableStateOf<String?>(null) }
var liquidationPriceLimit by remember { mutableStateOf<LiquidationPriceLimit?>(null) }
var isLiquidationLoading by remember { mutableStateOf(false) }
var errorInfo by remember { mutableStateOf<String?>(null) }
var isProcessing by remember { mutableStateOf(false) }
Expand Down Expand Up @@ -221,6 +222,7 @@ fun OpenPositionPage(
}

LaunchedEffect(usdtAmount, leverage, currentMarket.minAmount) {
liquidationPriceLimit = null
val amount = usdtAmount.toBigDecimalOrNull()
val minimumAmount = currentMarket.minAmount.toBigDecimalOrNull() ?: BigDecimal.ZERO
if (!shouldRequestLiquidationPrice(amount, minimumAmount)) {
Expand All @@ -238,7 +240,9 @@ fun OpenPositionPage(
.stripTrailingZeros()
.toPlainString()
.let { limitTradeInputDecimalPlaces(it, TRADE_INPUT_MAX_DECIMAL_PLACES) }
remoteLiquidationPrice = requestLiquidationPrice {
remoteLiquidationPrice = requestLiquidationPrice(
onLimitExceeded = { liquidationPriceLimit = it },
) {
viewModel.estimateLiquidationPrice(
marketId = currentMarket.marketId,
amount = normalizedAmount,
Expand Down Expand Up @@ -276,13 +280,29 @@ fun OpenPositionPage(
maximumMargin.stripTrailingZeros().toPlainString(),
currentToken?.symbol.orEmpty(),
)
val liquidationLimitMaxAmount = liquidationPriceLimit?.maxAmount
?.toBigDecimalOrNull()
?.takeIf { it > BigDecimal.ZERO }
?.stripTrailingZeros()
?.toPlainString()
val liquidationLimitAmountError = liquidationLimitMaxAmount?.let {
stringResource(R.string.perps_maximum_margin, it, currentToken?.symbol.orEmpty())
}
val liquidationLimitLeverageError = liquidationPriceLimit?.maxLeverage?.let {
stringResource(R.string.perps_maximum_leverage, it)
}
val liquidationLimitError = liquidationPriceLimit?.let {
listOfNotNull(liquidationLimitAmountError, liquidationLimitLeverageError)
.joinToString(". ")
.ifBlank { stringResource(R.string.error_perps_position_size_exceeds_leverage_limit) }
}
val displayLiquidationPrice = remoteLiquidationPrice
val marginLimitError = when {
belowMinimumMargin -> minimumMarginError
aboveMaximumMargin -> maximumMarginError
else -> null
}
val displayedErrorInfo = errorInfo?.takeIf { it.isNotBlank() } ?: marginLimitError
val displayedErrorInfo = errorInfo?.takeIf { it.isNotBlank() } ?: marginLimitError ?: liquidationLimitError
val tokenNetworkName = currentToken?.chainName
?.takeIf { it.isNotBlank() }
?: currentToken?.chainSymbol
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ class PerpetualViewModel @Inject constructor(
liquidationPriceResult(
price = null,
errorCode = response.errorCode,
limit = parseLiquidationPriceLimit(response.error?.extra),
)
}
} catch (e: CancellationException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ private fun PerpsAddContent(
val viewModel = hiltViewModel<PerpetualViewModel>()
var amount by remember(position.positionId) { mutableStateOf("") }
var remoteLiquidationPrice by remember(position.positionId) { mutableStateOf<String?>(null) }
var liquidationPriceLimit by remember(position.positionId) { mutableStateOf<LiquidationPriceLimit?>(null) }
var isLiquidationLoading by remember(position.positionId) { mutableStateOf(false) }
var liquidationJob by remember(position.positionId) { mutableStateOf<Job?>(null) }
val tokenBalance = selectedToken?.balance?.toBigDecimalOrNull() ?: BigDecimal.ZERO
Expand Down Expand Up @@ -305,6 +306,7 @@ private fun PerpsAddContent(
val priceScale = market?.priceScale ?: position.priceScale

LaunchedEffect(amount, belowMinimumMargin, aboveMaximumMargin) {
liquidationPriceLimit = null
val addMargin = amount.toBigDecimalOrNull()
if (!shouldRequestLiquidationPrice(addMargin, minimumMargin) || aboveMaximumMargin) {
liquidationJob?.cancel()
Expand All @@ -322,7 +324,9 @@ private fun PerpsAddContent(
.stripTrailingZeros()
.toPlainString()
.let { limitTradeInputDecimalPlaces(it, TRADE_INPUT_MAX_DECIMAL_PLACES) }
remoteLiquidationPrice = requestLiquidationPrice {
remoteLiquidationPrice = requestLiquidationPrice(
onLimitExceeded = { liquidationPriceLimit = it },
) {
viewModel.estimateLiquidationPrice(
amount = normalizedAmount,
positionId = position.positionId,
Expand All @@ -342,10 +346,26 @@ private fun PerpsAddContent(
maximumMargin.stripTrailingZeros().toPlainString(),
selectedToken?.symbol.orEmpty(),
)
val liquidationLimitMaxAmount = liquidationPriceLimit?.maxAmount
?.toBigDecimalOrNull()
?.takeIf { it > BigDecimal.ZERO }
?.stripTrailingZeros()
?.toPlainString()
val liquidationLimitAmountError = liquidationLimitMaxAmount?.let {
stringResource(R.string.perps_maximum_margin, it, selectedToken?.symbol.orEmpty())
}
val liquidationLimitLeverageError = liquidationPriceLimit?.maxLeverage?.let {
stringResource(R.string.perps_maximum_leverage, it)
}
val liquidationLimitError = liquidationPriceLimit?.let {
listOfNotNull(liquidationLimitAmountError, liquidationLimitLeverageError)
.joinToString(". ")
.ifBlank { stringResource(R.string.error_perps_position_size_exceeds_leverage_limit) }
}
val marginLimitError = when {
belowMinimumMargin -> minimumMarginError
aboveMaximumMargin -> maximumMarginError
else -> null
else -> liquidationLimitError
}

val currentPriceText = formatPerpsPrice(currentPrice, priceScale)
Expand Down
22 changes: 19 additions & 3 deletions app/src/main/java/one/mixin/android/util/ErrorHandler.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package one.mixin.android.util

import android.content.Context
import androidx.annotation.StringRes
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler
import one.mixin.android.MixinApplication
Expand Down Expand Up @@ -226,6 +227,8 @@ open class ErrorHandler {
const val MAX_WALLET_REACHED = 10632
const val PERPS_ORDER_VALUE_TOO_SMALL = 10650
const val PERPS_MARKET_ALREADY_HAS_ACTIVE_POSITION = 10651
const val PERPS_ORDER_VALUE_BELOW_MINIMUM = 10654
const val PERPS_POSITION_SIZE_EXCEEDS_LEVERAGE_LIMIT = 10655

const val UNSUPPORTED_WATCH_ADDRESS = 10633
const val INVALID_REFERRAL_CODE = 10730
Expand Down Expand Up @@ -270,6 +273,10 @@ fun Context.getMixinErrorStringByCode(
code: Int,
message: String,
): String {
perpsOrderValueErrorResource(code)?.let { resource ->
return getString(resource, code)
}

return when (code) {
ErrorHandler.TRANSACTION -> "${ErrorHandler.TRANSACTION} TRANSACTION"
ErrorHandler.BAD_DATA -> {
Expand Down Expand Up @@ -330,12 +337,12 @@ fun Context.getMixinErrorStringByCode(
ErrorHandler.MAX_WALLET_REACHED -> {
getString(R.string.error_too_many_wallets)
}
ErrorHandler.PERPS_ORDER_VALUE_TOO_SMALL -> {
getString(R.string.error_perps_order_value_too_small)
}
ErrorHandler.PERPS_MARKET_ALREADY_HAS_ACTIVE_POSITION -> {
getString(R.string.error_already_had_open_position)
}
ErrorHandler.PERPS_POSITION_SIZE_EXCEEDS_LEVERAGE_LIMIT -> {
getString(R.string.error_perps_position_size_exceeds_leverage_limit)
}
ErrorHandler.UNSUPPORTED_WATCH_ADDRESS -> {
getString(R.string.error_watch_address_not_supported)
}
Expand Down Expand Up @@ -442,3 +449,12 @@ fun Context.getMixinErrorStringByCode(
}
}
}

@StringRes
internal fun perpsOrderValueErrorResource(code: Int): Int? =
when (code) {
ErrorHandler.PERPS_ORDER_VALUE_TOO_SMALL,
ErrorHandler.PERPS_ORDER_VALUE_BELOW_MINIMUM,
-> R.string.error_perps_order_value_too_small
else -> null
}
2 changes: 2 additions & 0 deletions app/src/main/res/values-es/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@
<string name="error_old_version">ERROR 10006: Actualiza Mixin(%1$s) para seguir usando el servicio.</string>
<string name="error_open_location">No puedo encontrar una aplicación de mapas</string>
<string name="error_permission">Por favor abre los permisos necesarios</string>
<string name="error_perps_order_value_too_small">ERROR %1$d: El tamaño de la posición es demasiado pequeño.</string>
<string name="error_phone_invalid_format">ERROR 20110: Número de teléfono no válido</string>
<string name="error_phone_sms_delivery">ERROR 10003: Error al entregar SMS</string>
<string name="error_phone_verification_code_expired">ERROR 20114: Código de verificación del teléfono caducado</string>
Expand All @@ -419,6 +420,7 @@
<string name="error_unknown_with_code">ERROR: %1$d</string>
<string name="error_unknown_with_message">ERROR: %1$s</string>
<string name="error_used_phone">ERROR 20122: Este número de teléfono ya está asociado a otra cuenta.</string>
<string name="error_watch_address_not_supported">ERROR 10633: Actualmente no se admite añadir esta dirección</string>
<string name="error_user_invalid_format">ID de usuario invalido</string>
<string name="error_withdrawal_memo_format_incorrect">ERROR 20131: Formato de nota de retiro incorrecto.</string>
<string name="Everybody">Todos</string>
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values-in/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@
<string name="error_old_version">KESALAHAN 10006: Harap perbarui Mixin(%1$s) untuk terus menggunakan layanan.</string>
<string name="error_open_location">Tidak dapat menemukan aplikasi peta</string>
<string name="error_permission">Harap buka izin yang diperlukan</string>
<string name="error_perps_order_value_too_small">KESALAHAN %1$d: Ukuran posisi terlalu kecil.</string>
<string name="error_phone_invalid_format">KESALAHAN 20110: Nomor telepon tidak valid</string>
<string name="error_phone_sms_delivery">KESALAHAN 10003: Gagal mengirim SMS</string>
<string name="error_phone_verification_code_expired">KESALAHAN 20114: Kode verifikasi telepon sudah tidak berlaku</string>
Expand All @@ -265,6 +266,7 @@
<string name="error_unknown_with_code">KESALAHAN %1$d</string>
<string name="error_unknown_with_message">KESALAHAN: %1$s</string>
<string name="error_used_phone">KESALAHAN 20122: Ponsel digunakan oleh orang lain.</string>
<string name="error_watch_address_not_supported">ERROR 10633: Saat ini belum mendukung penambahan alamat ini</string>
<string name="error_user_invalid_format">ID pengguna tidak valid</string>
<string name="error_withdrawal_memo_format_incorrect">KESALAHAN 20131: Format memo penarikan salah.</string>
<string name="Everybody">Semua orang</string>
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values-ja/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@
<string name="error_old_version">エラー 10006:このサービスを引き続き使用するには、Mixin(%1$s)をアップデートしてください。</string>
<string name="error_open_location">地図アプリがありません</string>
<string name="error_permission">必要な権限を開いてください</string>
<string name="error_perps_order_value_too_small">エラー %1$d:ポジションサイズが小さすぎます。</string>
<string name="error_phone_invalid_format">エラー 20110:無効な電話番号です</string>
<string name="error_phone_sms_delivery">エラー 10003:SMSの送信に失敗しました</string>
<string name="error_phone_verification_code_expired">期限切れ</string>
Expand All @@ -394,6 +395,7 @@
<string name="error_unknown_with_code">エラー:%1$d</string>
<string name="error_unknown_with_message">エラー:%1$s</string>
<string name="error_used_phone">エラー20122:この電話番号はすでに他のアカウントと紐づけられています</string>
<string name="error_watch_address_not_supported">エラー 10633: このアドレスは現在対応していません</string>
<string name="error_user_invalid_format">無効なユーザーIDです</string>
<string name="error_withdrawal_memo_format_incorrect">エラー20131:出金メモのフォーマットが不正確です</string>
<string name="Everybody">全員</string>
Expand Down
6 changes: 4 additions & 2 deletions app/src/main/res/values-ko/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -484,9 +484,10 @@
<string name="error_invalid_quote_amount">오류 10614: 입력 금액이 너무 작거나 너무 큽니다. 금액을 조정하세요.</string>
<string name="error_invalid_quote_amount_detailed">오류 10614: 금액이 %1$s의 최대 허용 값을 초과했습니다. 금액을 조정해주세요. 지정가 주문을 해보시겠어요? 금액이나 토큰에 제한이 없습니다.</string>
<string name="error_no_available_quote">오류 10615: 이 거래 쌍은 현재 지원되지 않습니다. 다른 토큰으로 전환해 보십시오.</string>
<string name="error_perps_order_value_too_small">오류 10650: 위치 크기가 너무 작습니다.</string>
<string name="error_perps_order_value_too_small">오류 %1$d: 포지션 크기가 너무 작습니다.</string>
<string name="error_already_had_open_position">오류 10651: 이미 오픈 포지션이 있습니다.</string>
<string name="error_waiting_other_orders">오류 10652: 현재 시장에서 아직 다른 주문을 처리 중입니다. 나중에 다시 시도하십시오.</string>
<string name="error_perps_position_size_exceeds_leverage_limit">오류 10655: 포지션 크기가 선택한 레버리지 한도를 초과했습니다. 금액 또는 레버리지를 낮추세요.</string>
<string name="error_phone_verification_code_expired">오류 20114: 만료된 전화 확인 코드</string>
<string name="error_phone_verification_code_invalid">오류 20113: 잘못된 전화 확인 코드</string>
<string name="error_pin_check_too_many_request">5회 이상 시도하셨습니다. 다시 시도하려면 최소 24시간 정도 기다려 주십시오.</string>
Expand All @@ -509,7 +510,7 @@
<string name="error_unknown_with_code">오류 %1$d</string>
<string name="error_unknown_with_message">오류: %1$s</string>
<string name="error_used_phone">오류 20122: 이 전화번호는 이미 다른 계정과 연결되어 있습니다.</string>
<string name="error_watch_address_not_supported">오류 10633: 잘못된 주소</string>
<string name="error_watch_address_not_supported">오류 10633: 현재 이 주소를 추가할 수 없습니다</string>
<string name="error_invalid_referral_code">오류 10730: 잘못된 추천 코드</string>
<string name="error_already_bonded_referral_code">오류 10731: 이 계정은 이미 추천 코드를 적용했습니다.</string>
<string name="error_cannot_apply_your_own_referral_code">오류 10732: 자신의 추천 코드를 적용할 수 없습니다.</string>
Expand Down Expand Up @@ -1649,6 +1650,7 @@
<string name="single_transaction_should_be_between">금액은 %1$s %2$s에서 %3$s %4$s 사이여야 합니다.</string>
<string name="perps_minimum_margin">최소 금액은 %1$s %2$s입니다.</string>
<string name="perps_maximum_margin">최대 금액은 %1$s %2$s입니다.</string>
<string name="perps_maximum_leverage">이 금액의 최대 레버리지는 %1$dx입니다.</string>
<string name="web3_approve">승인</string>
<string name="web3_borrow">대출</string>
<string name="web3_burn">굽기</string>
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values-ms/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@
<string name="error_old_version">RALAT 10006: Sila kemas kini Mixin(%1$s) untuk terus menggunakan perkhidmatan ini.</string>
<string name="error_open_location">Tidak dapat mencari aplikasi peta</string>
<string name="error_permission">Sila buka kebenaran yang diperlukan</string>
<string name="error_perps_order_value_too_small">RALAT %1$d: Saiz posisi terlalu kecil.</string>
<string name="error_phone_invalid_format">RALAT 20110: Nombor telefon tidak sah</string>
<string name="error_phone_sms_delivery">RALAT 10003: Gagal menghantar SMS</string>
<string name="error_phone_verification_code_expired">RALAT 20114: Kod pengesahan telefon yang telah tamat tempoh</string>
Expand All @@ -284,6 +285,7 @@
<string name="error_unknown_with_code">RALAT %1$d</string>
<string name="error_unknown_with_message">RALAT: %1$s</string>
<string name="error_used_phone">RALAT 20122: Telefon digunakan oleh orang lain.</string>
<string name="error_watch_address_not_supported">RALAT 10633: Penambahan alamat ini tidak disokong buat masa ini</string>
<string name="error_user_invalid_format">Id pengguna tidak sah</string>
<string name="error_withdrawal_memo_format_incorrect">RALAT 20131: Penarikan format memo tidak betul.</string>
<string name="Everybody">Semua orang</string>
Expand Down
Loading