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
19 changes: 10 additions & 9 deletions app/src/main/java/one/mixin/android/extension/UrlExtension.kt
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ internal data class PerpsTradeAction(
)

internal data class PerpsOpenPositionAction(
val isLong: Boolean,
val isLong: Boolean?,
val leverage: Int?,
val margin: String?,
)
Expand Down Expand Up @@ -416,16 +416,15 @@ internal fun String.toPerpsTradeAction(): PerpsTradeAction? {
?: return if (!hasLeaderPositionId) PerpsTradeAction(null, null) else null
if (!marketId.isUUID()) return null

val isLong = if (query.queryParameter("action").equals("open", true)) {
when {
val isOpenAction = query.queryParameter("action").equals("open", true)
val openPosition = if (isOpenAction) {
val hasSide = query.hasQueryParameter("side")
val isLong = when {
!hasSide -> null
query.queryParameter("side").equals("long", true) -> true
query.queryParameter("side").equals("short", true) -> false
else -> null
else -> return 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
Expand All @@ -435,10 +434,12 @@ internal fun String.toPerpsTradeAction(): PerpsTradeAction? {
if (hasMargin && margin == null) return null

PerpsOpenPositionAction(
isLong = it,
isLong = isLong,
leverage = leverage,
margin = margin,
)
} else {
null
}
return PerpsTradeAction(marketId, leaderPositionId, openPosition)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ private fun resolveCurrentToken(
@Composable
fun OpenPositionPage(
market: PerpsMarket,
isLong: Boolean,
isLong: Boolean?,
source: String,
onBack: () -> Unit,
onOrderCreated: () -> Unit = {},
Expand All @@ -148,6 +148,7 @@ fun OpenPositionPage(
val acceptedPerpAssetIds = remember(acceptedPerpAssetIdsOrdered) { acceptedPerpAssetIdsOrdered.toSet() }

var currentMarket by remember(marketId) { mutableStateOf(market) }
var selectedIsLong by rememberSaveable(marketId, isLong) { mutableStateOf(isLong ?: true) }
var currentToken by remember { mutableStateOf<TokenItem?>(selectedToken) }
var availableTokens by remember { mutableStateOf<List<TokenItem>>(emptyList()) }
var usdtAmount by rememberSaveable(marketId, initialMargin) {
Expand Down Expand Up @@ -229,7 +230,7 @@ fun OpenPositionPage(
onCurrentTokenChange(currentToken)
}
val maxLeverage = currentMarket.leverage.coerceAtLeast(1)
LaunchedEffect(usdtAmount, leverage, currentToken?.assetId, takeProfitPrice, stopLossPrice) {
LaunchedEffect(selectedIsLong, usdtAmount, leverage, currentToken?.assetId, takeProfitPrice, stopLossPrice) {
errorInfo = null
}
LaunchedEffect(maxLeverage, marketId) {
Expand All @@ -242,7 +243,7 @@ fun OpenPositionPage(
}
}

LaunchedEffect(usdtAmount, leverage, currentMarket.minAmount) {
LaunchedEffect(selectedIsLong, usdtAmount, leverage, currentMarket.minAmount) {
val amount = usdtAmount.toBigDecimalOrNull()
val minimumAmount = currentMarket.minAmount.toBigDecimalOrNull() ?: BigDecimal.ZERO
if (!shouldRequestLiquidationPrice(amount, minimumAmount)) {
Expand All @@ -264,7 +265,7 @@ fun OpenPositionPage(
viewModel.estimateLiquidationPrice(
marketId = currentMarket.marketId,
amount = normalizedAmount,
side = if (isLong) "long" else "short",
side = if (selectedIsLong) "long" else "short",
leverage = leverage.toInt(),
)
}
Expand Down Expand Up @@ -327,7 +328,7 @@ fun OpenPositionPage(
stopLossPrice
},
currentPrice = currentMarket.last,
isLong = isLong,
isLong = selectedIsLong,
marketIconUrl = currentMarket.iconUrl,
marketSymbol = currentMarket.tokenSymbol,
marginAmount = usdtAmount,
Expand Down Expand Up @@ -374,6 +375,13 @@ fun OpenPositionPage(
.verticalScroll(rememberScrollState())
.padding(horizontal = 16.dp)
) {
if (isLong == null) {
PerpsSideSelector(
isLong = selectedIsLong,
onSideChange = { selectedIsLong = it },
)
Spacer(modifier = Modifier.height(16.dp))
}
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
Expand All @@ -389,7 +397,7 @@ fun OpenPositionPage(
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(
text = "${if (isLong) stringResource(R.string.Long) else stringResource(R.string.Short)} ${currentMarket.tokenSymbol}",
text = "${if (selectedIsLong) stringResource(R.string.Long) else stringResource(R.string.Short)} ${currentMarket.tokenSymbol}",
fontSize = 16.sp,
color = MixinAppTheme.colors.textPrimary
)
Expand Down Expand Up @@ -530,7 +538,7 @@ fun OpenPositionPage(
currentLeverage = leverage,
maxLeverage = maxLeverage,
amount = usdtAmount,
isLong = isLong
isLong = selectedIsLong
).setOnLeverageSelected { newLeverage ->
leverage = newLeverage
context.defaultSharedPreferences.putInt(getLeveragePrefKey(marketId), newLeverage.toInt())
Expand Down Expand Up @@ -564,7 +572,7 @@ fun OpenPositionPage(
currentLeverage = leverage,
maxLeverage = maxLeverage,
amount = usdtAmount,
isLong = isLong
isLong = selectedIsLong
).setOnLeverageSelected { newLeverage ->
leverage = newLeverage
context.defaultSharedPreferences.putInt(getLeveragePrefKey(marketId), newLeverage.toInt())
Expand Down Expand Up @@ -623,7 +631,7 @@ fun OpenPositionPage(
currentLeverage = leverage,
maxLeverage = maxLeverage,
amount = usdtAmount,
isLong = isLong
isLong = selectedIsLong
).setOnLeverageSelected { newLeverage ->
leverage = newLeverage
context.defaultSharedPreferences.putInt(getLeveragePrefKey(marketId), newLeverage.toInt())
Expand Down Expand Up @@ -660,7 +668,7 @@ fun OpenPositionPage(
val profitInfo = calculateProfitInfo(
amount = usdtAmount,
leverage = leverage,
isLong = isLong,
isLong = selectedIsLong,
priceChangePercent = 1.0,
)

Expand Down Expand Up @@ -798,7 +806,7 @@ fun OpenPositionPage(
viewModel.openPerpsOrder(
assetId = token.assetId,
marketId = m.marketId,
side = if (isLong) "long" else "short",
side = if (selectedIsLong) "long" else "short",
amount = normalizedAmount,
leverage = leverage.toInt(),
walletId = walletId,
Expand All @@ -812,7 +820,7 @@ fun OpenPositionPage(
PerpsConfirmBottomSheetDialogFragment.newInstance(
marketSymbol = m.displaySymbol,
marketIcon = m.iconUrl,
isLong = isLong,
isLong = selectedIsLong,
amount = response.payAmount,
leverage = leverage.toInt(),
entryPrice = m.last,
Expand Down Expand Up @@ -910,6 +918,42 @@ fun OpenPositionPage(

}

@Composable
private fun PerpsSideSelector(
isLong: Boolean,
onSideChange: (Boolean) -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.background(MixinAppTheme.colors.backgroundGrayLight)
.padding(4.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
listOf(true to R.string.Long, false to R.string.Short).forEach { (sideIsLong, labelRes) ->
val selected = sideIsLong == isLong
Box(
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(6.dp))
.background(
if (selected) MixinAppTheme.colors.accent else Color.Transparent,
)
.clickable { onSideChange(sideIsLong) }
.padding(vertical = 10.dp),
contentAlignment = Alignment.Center,
) {
Text(
text = stringResource(labelRes),
fontSize = 14.sp,
color = if (selected) Color.White else MixinAppTheme.colors.textPrimary,
)
}
}
}
}

internal fun resolveInitialPerpsLeverage(
requestedLeverage: Int?,
savedLeverage: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ class PerpsActivity : BaseActivity() {
marketSymbol: String,
marketDisplaySymbol: String,
marketTokenSymbol: String = "",
isLong: Boolean,
isLong: Boolean?,
source: String,
returnToDetail: Boolean = false,
leaderPositionId: String? = null,
Expand All @@ -126,7 +126,7 @@ class PerpsActivity : BaseActivity() {
putExtra(EXTRA_MARKET_DISPLAY_SYMBOL, marketDisplaySymbol)
putExtra(EXTRA_MARKET_TOKEN_SYMBOL, marketTokenSymbol)
putExtra(EXTRA_MODE, MODE_OPEN_POSITION)
putExtra(EXTRA_IS_LONG, isLong)
isLong?.let { putExtra(EXTRA_IS_LONG, it) }
putExtra(EXTRA_SOURCE, source)
putExtra(EXTRA_RETURN_TO_DETAIL, returnToDetail)
leaderPositionId?.let { putExtra(EXTRA_LEADER_POSITION_ID, it) }
Expand Down Expand Up @@ -162,7 +162,9 @@ class PerpsActivity : BaseActivity() {
val displaySymbolExtra = currentIntent.getStringExtra(EXTRA_MARKET_DISPLAY_SYMBOL).orEmpty()
val tokenSymbolExtra = currentIntent.getStringExtra(EXTRA_MARKET_TOKEN_SYMBOL).orEmpty()
val mode = currentIntent.getStringExtra(EXTRA_MODE) ?: MODE_DETAIL
val isLong = currentIntent.getBooleanExtra(EXTRA_IS_LONG, true)
val isLong = currentIntent
.takeIf { it.hasExtra(EXTRA_IS_LONG) }
?.getBooleanExtra(EXTRA_IS_LONG, true)
val source = currentIntent.getStringExtra(EXTRA_SOURCE) ?: AnalyticsTracker.PerpsSource.PERPS_MARKET_DETAIL
val returnToDetail = currentIntent.getBooleanExtra(EXTRA_RETURN_TO_DETAIL, false)
leaderPositionId = currentIntent.getStringExtra(EXTRA_LEADER_POSITION_ID)
Expand Down Expand Up @@ -196,13 +198,15 @@ class PerpsActivity : BaseActivity() {
marketDisplaySymbol = market.displaySymbol,
marketTokenSymbol = market.tokenSymbol,
source = source,
reuseCurrentActivity = false,
leaderPositionId = leaderPositionId,
)
finish()
}
return@launch
}
AnalyticsTracker.trackPerpsOpenStart(
direction = if (isLong) AnalyticsTracker.PerpsDirection.LONG else AnalyticsTracker.PerpsDirection.SHORT,
direction = if (isLong == false) AnalyticsTracker.PerpsDirection.SHORT else AnalyticsTracker.PerpsDirection.LONG,
source = source,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ internal sealed interface WalletHomeBannerActionTarget {
) : WalletHomeBannerActionTarget
data class PerpsOpen(
val marketId: String,
val isLong: Boolean,
val isLong: Boolean?,
val leverage: Int?,
val margin: String?,
val leaderPositionId: String?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,20 @@ class PerpsTradeActionTest {
}

@Test
fun directPerpsOpenRequiresOpenActionAndValidSide() {
fun directPerpsOpenRequiresOpenActionAndRejectsInvalidSide() {
val base = "https://mixin.one/trade?type=perps&market=e015f42e-b0ff-38e7-87b1-7e8d46fea119&leader_position=45d4c134-5682-4b1a-baf5-7c73b1590cc1"

assertNull("$base&side=long".toPerpsTradeAction()?.openPosition)
assertNull("$base&action=open".toPerpsTradeAction()?.openPosition)
assertEquals(
PerpsOpenPositionAction(
isLong = null,
leverage = null,
margin = null,
),
"$base&action=open".toPerpsTradeAction()?.openPosition,
)
assertNull("$base&action=close&side=long".toPerpsTradeAction()?.openPosition)
assertNull("$base&action=open&side=invalid".toPerpsTradeAction()?.openPosition)
assertNull("$base&action=open&side=invalid".toPerpsTradeAction())
assertEquals(
"45d4c134-5682-4b1a-baf5-7c73b1590cc1",
"$base&action=open".toPerpsTradeAction()?.leaderPositionId,
Expand Down Expand Up @@ -256,6 +263,20 @@ class PerpsTradeActionTest {
)
}

@Test
fun classicBannerActionParsesIncompletePerpsOpenTarget() {
assertEquals(
WalletHomeBannerActionTarget.PerpsOpen(
marketId = "e015f42e-b0ff-38e7-87b1-7e8d46fea119",
isLong = null,
leverage = null,
margin = null,
leaderPositionId = "45d4c134-5682-4b1a-baf5-7c73b1590cc1",
),
"https://mixin.one/trade?type=perps&market=e015f42e-b0ff-38e7-87b1-7e8d46fea119&action=open&leader_position=45d4c134-5682-4b1a-baf5-7c73b1590cc1".toClassicWalletHomeBannerActionTarget(),
)
}

@Test
fun classicBannerActionFallsBackToWebTarget() {
val url = "https://mixin.one/users/41d16c28-0c3a-493d-a2b4-b57875371abf"
Expand Down