Skip to content
Merged
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 @@ -20,7 +20,9 @@ data class OpenOrderRequest(
@SerializedName("stop_loss_price")
val stopLossPrice: String? = null,
@SerializedName("destination")
val destination: String? = null
val destination: String? = null,
@SerializedName("leader_position_id")
val leaderPositionId: String? = null,
)

data class OpenOrderResponse(
Expand Down
98 changes: 88 additions & 10 deletions app/src/main/java/one/mixin/android/extension/UrlExtension.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import one.mixin.android.ui.conversation.link.LinkBottomSheetDialogFragment
import one.mixin.android.ui.device.ConfirmBottomFragment
import one.mixin.android.ui.forward.ForwardActivity
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.Companion.PREF_TRADE_SELECTED_TAB_PREFIX
import one.mixin.android.ui.home.web3.trade.TradeFragment.Companion.TAB_PERPETUAL
import one.mixin.android.ui.home.web3.trade.perps.PerpsActivity
Expand All @@ -56,6 +57,7 @@ import one.mixin.android.vo.getShareCategory
import one.mixin.android.web3.js.Web3Signer
import one.mixin.android.widget.gallery.MimeType
import timber.log.Timber
import java.math.BigDecimal
import java.net.URI
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
Expand Down Expand Up @@ -378,6 +380,14 @@ fun String.checkTradeUrl(

internal data class PerpsTradeAction(
val marketId: String?,
val leaderPositionId: String?,
val openPosition: PerpsOpenPositionAction? = null,
)

internal data class PerpsOpenPositionAction(
val isLong: Boolean,
val leverage: Int?,
val margin: String?,
)

internal data class SpotTradeAction(
Expand All @@ -399,9 +409,38 @@ internal fun String.toPerpsTradeAction(): PerpsTradeAction? {
return null
}

val marketId = query.queryParameter("market")?.takeIf(String::isNotBlank) ?: return PerpsTradeAction(null)
val hasLeaderPositionId = query.hasQueryParameter("leader_position")
val leaderPositionId = query.queryParameter("leader_position")
if (hasLeaderPositionId && (leaderPositionId == null || !leaderPositionId.isUUID())) return null
val marketId = query.queryParameter("market")?.takeIf(String::isNotBlank)
?: return if (!hasLeaderPositionId) PerpsTradeAction(null, null) else null
if (!marketId.isUUID()) return null
return PerpsTradeAction(marketId)

val isLong = if (query.queryParameter("action").equals("open", true)) {
when {
query.queryParameter("side").equals("long", true) -> true
query.queryParameter("side").equals("short", true) -> false
else -> null
}
} else {
null
}
val openPosition = isLong?.let {
val hasLeverage = query.hasQueryParameter("leverage")
val leverage = query.queryParameter("leverage")?.toIntOrNull()?.takeIf { value -> value > 0 }
if (hasLeverage && leverage == null) return null

val hasMargin = query.hasQueryParameter("margin")
val margin = query.queryParameter("margin")?.toPerpsMarginOrNull()
if (hasMargin && margin == null) return null

PerpsOpenPositionAction(
isLong = it,
leverage = leverage,
margin = margin,
)
}
return PerpsTradeAction(marketId, leaderPositionId, openPosition)
}

internal fun String.toSpotTradeAction(): SpotTradeAction? {
Expand Down Expand Up @@ -487,14 +526,31 @@ private suspend fun openLocalPerpsTradeAction(
PerpsDatabase.getDatabase(context, identityNumber).perpsMarketDao().getMarket(marketId)
} ?: return false

PerpsActivity.showDetail(
context,
market.marketId,
market.displaySymbol,
market.displaySymbol,
market.tokenSymbol,
tradeSource(context),
)
val openPosition = action.openPosition
if (openPosition == null) {
PerpsActivity.showDetail(
context,
market.marketId,
market.displaySymbol,
market.displaySymbol,
market.tokenSymbol,
tradeSource(context),
leaderPositionId = action.leaderPositionId,
)
} else {
PerpsActivity.showOpenPosition(
context = context,
marketId = market.marketId,
marketSymbol = market.displaySymbol,
marketDisplaySymbol = market.displaySymbol,
marketTokenSymbol = market.tokenSymbol,
isLong = openPosition.isLong,
source = tradeSource(context),
leaderPositionId = action.leaderPositionId,
initialLeverage = openPosition.leverage,
initialMargin = openPosition.margin,
)
}
closeSourceWebActivityIfNeeded(context)
return true
}
Expand Down Expand Up @@ -539,9 +595,31 @@ private fun String.queryParameter(name: String): String? =
}
.firstOrNull()

private fun String.hasQueryParameter(name: String): Boolean =
split("&").any { parameter ->
parameter.substringBefore("=").urlDecode() == name
}

private fun String.urlDecode(): String? =
runCatching { URLDecoder.decode(this, StandardCharsets.UTF_8.name()) }.getOrNull()

private fun String.toPerpsMarginOrNull(): String? {
if (isEmpty() || length > 64) return null
var hasDigit = false
var hasDecimalPoint = false
for (character in this) {
when {
character.isDigit() -> hasDigit = true
character == '.' && !hasDecimalPoint -> hasDecimalPoint = true
else -> return null
}
}
if (!hasDigit) return null
val amount = toBigDecimalOrNull()?.stripTrailingZeros() ?: return null
if (amount <= BigDecimal.ZERO || amount.scale() > TRADE_INPUT_MAX_DECIMAL_PLACES) return null
return amount.toPlainString()
}

private fun getUserOrAppNotFoundTip(isApp: Boolean) =
if (isApp) R.string.Bot_not_found else R.string.User_not_found

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import one.mixin.android.extension.isValidStartParam
import one.mixin.android.extension.putInt
import one.mixin.android.extension.stripAmountZero
import one.mixin.android.extension.toast
import one.mixin.android.extension.toPerpsTradeAction
import one.mixin.android.extension.withArgs
import one.mixin.android.job.MixinJobManager
import one.mixin.android.job.RefreshAssetsJob
Expand Down Expand Up @@ -1105,8 +1106,13 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() {
val type = uri.getQueryParameter("type")

if (type.equals("perps", true) || type.equals("perpetual", true)) {
val marketId = uri.getQueryParameter("market")
if (marketId.isNullOrBlank() || !marketId.isUUID()) {
val action = uri.toString().toPerpsTradeAction()
if (action == null) {
showError(R.string.Data_error)
return
}
val marketId = action.marketId
if (marketId == null) {
defaultSharedPreferences.putInt(
"$PREF_TRADE_SELECTED_TAB_PREFIX${Session.getAccountId() ?: ""}",
TAB_PERPETUAL,
Expand All @@ -1127,18 +1133,36 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() {
return
}

PerpsActivity.showDetail(
requireContext(),
market.marketId,
market.displaySymbol,
market.displaySymbol,
market.tokenSymbol,
if (activity is ConversationActivity) {
AnalyticsTracker.MarketDetailSource.APP_CARD
} else {
AnalyticsTracker.MarketDetailSource.SCHEMA
},
)
val source = if (activity is ConversationActivity) {
AnalyticsTracker.MarketDetailSource.APP_CARD
} else {
AnalyticsTracker.MarketDetailSource.SCHEMA
}
val openPosition = action.openPosition
if (openPosition == null) {
PerpsActivity.showDetail(
requireContext(),
market.marketId,
market.displaySymbol,
market.displaySymbol,
market.tokenSymbol,
source,
leaderPositionId = action.leaderPositionId,
)
} else {
PerpsActivity.showOpenPosition(
context = requireContext(),
marketId = market.marketId,
marketSymbol = market.displaySymbol,
marketDisplaySymbol = market.displaySymbol,
marketTokenSymbol = market.tokenSymbol,
isLong = openPosition.isLong,
source = source,
leaderPositionId = action.leaderPositionId,
initialLeverage = openPosition.leverage,
initialMargin = openPosition.margin,
)
}
closeSourceWebActivityIfNeeded()
dismiss()
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand Down Expand Up @@ -127,10 +128,14 @@ fun OpenPositionPage(
isLong: Boolean,
source: String,
onBack: () -> Unit,
onOrderCreated: () -> Unit = {},
onOpenSuccess: (String) -> Unit = { onBack() },
selectedToken: TokenItem?,
onTokenSelect: () -> Unit = {},
onCurrentTokenChange: (TokenItem?) -> Unit = {},
leaderPositionId: String? = null,
initialLeverage: Int? = null,
initialMargin: String? = null,
) {
val context = LocalContext.current
val waitingOtherOrdersError = stringResource(R.string.error_waiting_other_orders)
Expand All @@ -145,7 +150,14 @@ fun OpenPositionPage(
var currentMarket by remember(marketId) { mutableStateOf(market) }
var currentToken by remember { mutableStateOf<TokenItem?>(selectedToken) }
var availableTokens by remember { mutableStateOf<List<TokenItem>>(emptyList()) }
var usdtAmount by remember { mutableStateOf("") }
var usdtAmount by rememberSaveable(marketId, initialMargin) {
mutableStateOf(
limitTradeInputDecimalPlaces(
initialMargin.orEmpty(),
TRADE_INPUT_MAX_DECIMAL_PLACES,
),
)
}
var takeProfitPrice by remember { mutableStateOf("") }
var stopLossPrice by remember { mutableStateOf("") }
var remoteLiquidationPrice by remember { mutableStateOf<String?>(null) }
Expand All @@ -160,7 +172,15 @@ fun OpenPositionPage(
.getInt(getLeveragePrefKey(marketId), DEFAULT_LEVERAGE)
.coerceAtLeast(1)
}
var leverage by remember(marketId) { mutableFloatStateOf(savedLeverage.toFloat()) }
var leverage by rememberSaveable(marketId, initialLeverage) {
mutableFloatStateOf(
resolveInitialPerpsLeverage(
requestedLeverage = initialLeverage,
savedLeverage = savedLeverage,
maxLeverage = market.leverage,
).toFloat(),
)
}

LaunchedEffect(marketId) {
while (true) {
Expand Down Expand Up @@ -216,7 +236,9 @@ fun OpenPositionPage(
val boundedLeverage = leverage.coerceIn(1f, maxLeverage.toFloat())
if (boundedLeverage != leverage) {
leverage = boundedLeverage
context.defaultSharedPreferences.putInt(getLeveragePrefKey(marketId), boundedLeverage.toInt())
if (initialLeverage == null) {
context.defaultSharedPreferences.putInt(getLeveragePrefKey(marketId), boundedLeverage.toInt())
}
}
}

Expand Down Expand Up @@ -783,8 +805,10 @@ fun OpenPositionPage(
// Null means "leave TP/SL unset" when creating a new position.
takeProfitPrice = takeProfitPrice.takeIf { it.isNotBlank() },
stopLossPrice = stopLossPrice.takeIf { it.isNotBlank() },
leaderPositionId = leaderPositionId,
entryPrice = m.last,
onSuccess = { response ->
onOrderCreated()
PerpsConfirmBottomSheetDialogFragment.newInstance(
marketSymbol = m.displaySymbol,
marketIcon = m.iconUrl,
Expand Down Expand Up @@ -886,6 +910,12 @@ fun OpenPositionPage(

}

internal fun resolveInitialPerpsLeverage(
requestedLeverage: Int?,
savedLeverage: Int,
maxLeverage: Int,
): Int = (requestedLeverage ?: savedLeverage).coerceIn(1, maxLeverage.coerceAtLeast(1))

private fun generateLeverageOptions(maxLeverage: Int): List<Int> {
val safeMaxLeverage = maxLeverage.coerceAtLeast(1)
val baseOptions = listOf(5, 10, 20)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,7 @@ class PerpetualViewModel @Inject constructor(
takeProfitPrice: String? = null,
stopLossPrice: String? = null,
destination: String? = null,
leaderPositionId: String? = null,
entryPrice: String,
onSuccess: (OpenOrderResponse) -> Unit,
onError: (Int, String) -> Unit
Expand All @@ -459,7 +460,8 @@ class PerpetualViewModel @Inject constructor(
walletId = walletId,
takeProfitPrice = takeProfitPrice,
stopLossPrice = stopLossPrice,
destination = destination
destination = destination,
leaderPositionId = leaderPositionId,
)

val response = withContext(Dispatchers.IO) {
Expand Down
Loading