Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
bbed1ef
Add autoclose domain types for perpetual TP/SL modify
gemdev111 May 27, 2026
bfa45d1
Add autoclose UI model and factory
gemdev111 May 27, 2026
af8aa15
Add coordinator support for perpetual modify
gemdev111 May 27, 2026
bac31cc
Add shared autoclose UI components
gemdev111 May 27, 2026
ea32401
Add autoclose modify viewmodel and scene
gemdev111 May 27, 2026
41c171c
Wire perpetual position autoclose entry point and route
gemdev111 May 27, 2026
a6aa97a
Add open-time autoclose support
gemdev111 May 27, 2026
7379d33
Add modify autoclose summary to confirm screen
gemdev111 May 27, 2026
9d50ff4
Extract NavEntryViewModelStoreOwner to shared :ui utility
gemdev111 May 28, 2026
53bd0b5
Suppress autoclose PnL when trigger price is directionally invalid
gemdev111 May 28, 2026
273385d
Keep GemTextField caret at end on external value updates
gemdev111 May 28, 2026
0fc6e14
Add asset header and polish AmountAutocloseSheet open flow
gemdev111 May 28, 2026
8a8fd6c
Restructure autoclose modify flow as nested nav graph
gemdev111 May 28, 2026
3e258a7
Merge branch 'main' into autoclose-android
gemdev111 May 28, 2026
cda64f8
Match autoclose sheet height to modal-sheet convention
gemdev111 May 28, 2026
3f60deb
Merge branch 'main' into autoclose-android
gemdev111 May 28, 2026
f2d1db8
Replace core submodule with main's vendored core (pre-merge, local)
gemdev111 Jun 1, 2026
05b39f6
Merge branch 'main' into autoclose-android
gemdev111 Jun 1, 2026
a5593e0
Fix AppIcons
gemdev111 Jun 1, 2026
e841ae5
Parse Auto Close TP/SL prices locale-aware
gemdev111 Jun 1, 2026
5af015b
Consolidate Auto Close scene to single onAction
gemdev111 Jun 1, 2026
1efc26b
Fix Auto Close chevron spacing
gemdev111 Jun 1, 2026
795b7e6
Defer Auto Close validation until submit
gemdev111 Jun 1, 2026
65ec7c3
Tidy Auto Close estimator and validator
gemdev111 Jun 1, 2026
223aa24
Localize Auto Close trigger errors and separate them from the field
gemdev111 Jun 1, 2026
01a6e86
Update PerpetualError.swift
gemdev111 Jun 1, 2026
eaccc80
Add Auto Close defaults and suggestion tiers to perpetual config
gemdev111 Jun 2, 2026
fb81ee5
Source Auto Close suggestions and options from core
gemdev111 Jun 2, 2026
d22925d
Generalize leverage picker into reusable WheelPickerSheet
gemdev111 Jun 2, 2026
4895d61
Add default Take Profit and Stop Loss preferences on iOS
gemdev111 Jun 2, 2026
3516134
Prefill Auto Close from defaults and recompute on leverage change
gemdev111 Jun 2, 2026
f9ee162
Add default Take Profit and Stop Loss preferences on Android
gemdev111 Jun 2, 2026
68661c3
Prefill Auto Close from defaults via trigger flows
gemdev111 Jun 2, 2026
578875b
Show Auto Close suggestions only when the field is empty
gemdev111 Jun 2, 2026
8a4b88b
Support indented LinkItem; reorder Perpetual VM
gemdev111 Jun 2, 2026
efe1a83
Merge branch 'main' into autoclose-android
gemdev111 Jun 2, 2026
78a4d98
Remove Done button from Auto Close suggestions bar
gemdev111 Jun 2, 2026
199ae34
Extend stop-loss percent options with 3% and 5%
gemdev111 Jun 2, 2026
cfc49e3
Show Perpetuals settings without developer mode on Android
gemdev111 Jun 2, 2026
07acf1c
React to Perpetuals toggle for data sync on Android
gemdev111 Jun 2, 2026
e382d1b
Merge branch 'main' into autoclose-android
gemdev111 Jun 3, 2026
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 @@ -3,18 +3,11 @@ package com.gemwallet.android.ui.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.lifecycle.DEFAULT_ARGS_KEY
import androidx.lifecycle.HasDefaultViewModelProviderFactory
import androidx.lifecycle.SAVED_STATE_REGISTRY_OWNER_KEY
import androidx.lifecycle.SavedStateViewModelFactory
import androidx.lifecycle.VIEW_MODEL_STORE_OWNER_KEY
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.enableSavedStateHandles
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.MutableCreationExtras
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavEntryDecorator
Expand All @@ -23,12 +16,12 @@ import androidx.navigation3.runtime.NavMetadataKey
import androidx.navigation3.runtime.get
import androidx.navigation3.runtime.metadata
import androidx.savedstate.SavedState
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.compose.LocalSavedStateRegistryOwner
import androidx.savedstate.savedState
import com.gemwallet.android.ext.toIdentifier
import com.gemwallet.android.ui.models.navigation.RouteArgument
import com.gemwallet.android.ui.viewmodel.NavEntryViewModelStoreOwner
import com.wallet.core.primitives.AssetId
import kotlin.reflect.KClass

Expand Down Expand Up @@ -110,7 +103,7 @@ private fun rememberEntryViewModelStoreOwner(
entryViewModelStores.store(contentKey)
}
return remember(parent, store, defaultArgs, savedStateRegistryOwner) {
RouteArgumentsViewModelStoreOwner(
NavEntryViewModelStoreOwner(
parent = parent,
store = store,
savedStateRegistryOwner = checkNotNull(savedStateRegistryOwner) {
Expand Down Expand Up @@ -158,43 +151,6 @@ private object EntryViewModelStoresFactory : ViewModelProvider.Factory {
}
}

private class RouteArgumentsViewModelStoreOwner(
private val parent: ViewModelStoreOwner,
private val store: ViewModelStore,
private val savedStateRegistryOwner: SavedStateRegistryOwner,
private val defaultArgs: SavedState,
) : ViewModelStoreOwner,
SavedStateRegistryOwner,
HasDefaultViewModelProviderFactory {

init {
enableSavedStateHandles()
}

override val viewModelStore: ViewModelStore
get() = store

override val savedStateRegistry: SavedStateRegistry
get() = savedStateRegistryOwner.savedStateRegistry

override val lifecycle
get() = savedStateRegistryOwner.lifecycle

override val defaultViewModelProviderFactory: ViewModelProvider.Factory
get() = (parent as? HasDefaultViewModelProviderFactory)?.defaultViewModelProviderFactory
?: SavedStateViewModelFactory()

override val defaultViewModelCreationExtras: CreationExtras
get() = MutableCreationExtras(
(parent as? HasDefaultViewModelProviderFactory)?.defaultViewModelCreationExtras
?: CreationExtras.Empty
).apply {
this[SAVED_STATE_REGISTRY_OWNER_KEY] = this@RouteArgumentsViewModelStoreOwner
this[VIEW_MODEL_STORE_OWNER_KEY] = this@RouteArgumentsViewModelStoreOwner
this[DEFAULT_ARGS_KEY] = defaultArgs
}
}

internal fun NavEntry<NavKey>.withOccurrenceContentKey(
key: NavKey,
occurrence: Int,
Expand All @@ -209,3 +165,4 @@ internal fun NavEntry<NavKey>.withOccurrenceContentKey(
entry.Content()
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.navigation3.runtime.NavKey
import com.gemwallet.android.features.perpetual.views.market.PerpetualMarketNavScreen
import com.gemwallet.android.features.perpetual.views.position.PerpetualPositionNavScreen
import com.gemwallet.android.ui.models.actions.AmountTransactionAction
import com.gemwallet.android.ui.models.actions.AssetIdAction
import com.gemwallet.android.ui.models.actions.ConfirmTransactionAction
import com.gemwallet.android.ui.navigation.assetIdArgument
import com.gemwallet.android.ui.navigation.routeArguments
Expand All @@ -20,15 +21,15 @@ data class PerpetualPositionRoute(val assetId: AssetId) : NavKey

fun EntryProviderScope<NavKey>.perpetualScreen(
onCancel: () -> Unit,
onOpenPerpetualDetails: (AssetId) -> Unit,
onOpenPerpetualDetails: AssetIdAction,
amountAction: AmountTransactionAction,
confirmAction: ConfirmTransactionAction,
onTransaction: (TransactionId) -> Unit,
) {
entry<PerpetualRoute> {
PerpetualMarketNavScreen(
onOpenPerpetualDetails = onOpenPerpetualDetails,
onCancel = onCancel
onCancel = onCancel,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import com.wallet.core.primitives.PerpetualData
import com.wallet.core.primitives.PerpetualDirection
import com.wallet.core.primitives.PerpetualId
import com.wallet.core.primitives.PerpetualMarginType
import com.wallet.core.primitives.PerpetualModifyConfirmData
import com.wallet.core.primitives.PerpetualModifyPositionType
import com.wallet.core.primitives.PerpetualPosition
import com.wallet.core.primitives.PerpetualType
import kotlinx.coroutines.flow.firstOrNull
Expand Down Expand Up @@ -63,6 +65,27 @@ class BuildPerpetualParamsImpl(
.perpetual(PerpetualType.Close(confirmData))
}

override suspend fun modify(
perpetualId: PerpetualId,
modifyTypes: List<PerpetualModifyPositionType>,
takeProfitOrderId: ULong?,
stopLossOrderId: ULong?,
): ConfirmParams.PerpetualParams? {
if (modifyTypes.isEmpty()) return null
val data = getPerpetual(perpetualId) ?: return null
val assetIndex = data.perpetual.identifier.toIntOrNull() ?: return null
val account = sessionRepository.session().value?.wallet?.hyperliquidAccount ?: return null
val confirmData = PerpetualModifyConfirmData(
baseAsset = HypercoreUSDC,
assetIndex = assetIndex,
modifyTypes = modifyTypes,
takeProfitOrderId = takeProfitOrderId?.toLong(),
stopLossOrderId = stopLossOrderId?.toLong(),
)
return ConfirmParams.Builder(data.asset, account)
.perpetual(PerpetualType.Modify(confirmData))
}

private suspend fun getPerpetual(perpetualId: PerpetualId): PerpetualData? =
perpetualRepository.getPerpetual(perpetualId).firstOrNull()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,24 @@ class UserConfig(
}
}

fun perpetualTakeProfit(): Flow<Int> = context.dataStore.data
.map { preferences -> preferences[Key.PerpetualTakeProfit] ?: PerpetualConfig.defaultTakeProfit }

suspend fun setPerpetualTakeProfit(value: Int) {
context.dataStore.edit { preferences ->
preferences[Key.PerpetualTakeProfit] = value
}
}

fun perpetualStopLoss(): Flow<Int> = context.dataStore.data
.map { preferences -> preferences[Key.PerpetualStopLoss] ?: PerpetualConfig.defaultStopLoss }

suspend fun setPerpetualStopLoss(value: Int) {
context.dataStore.edit { preferences ->
preferences[Key.PerpetualStopLoss] = value
}
}

fun getLatestAppUpdate(): Flow<AppUpdateInfo?> = context.dataStore.data
.map { preferences ->
val version = preferences[Key.LatestVersion].orEmpty()
Expand Down Expand Up @@ -206,6 +224,8 @@ class UserConfig(
val AskNotifications = longPreferencesKey("ask_notifications")
val IsPerpetualEnabled = booleanPreferencesKey("is_perpetual_enabled")
val PerpetualLeverage = intPreferencesKey("perpetual_leverage")
val PerpetualTakeProfit = intPreferencesKey("perpetual_take_profit")
val PerpetualStopLoss = intPreferencesKey("perpetual_stop_loss")
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.gemwallet.android.application.perpetual.coordinators.SyncPerpetuals
import com.gemwallet.android.data.repositories.config.UserConfig
import com.gemwallet.android.data.repositories.session.SessionRepository
import com.gemwallet.android.ext.hasPerpetualsSupport
import com.gemwallet.android.model.Session
import com.gemwallet.android.data.services.gemapi.http.DeviceRequestSigner
import com.gemwallet.android.serializer.StreamEventSerializer
import com.gemwallet.android.serializer.jsonEncoder
Expand All @@ -28,8 +29,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString

Expand Down Expand Up @@ -64,12 +67,14 @@ class StreamObserverService(
subscriptionService.setupAssets(wallet.id)
if (connectionJob == null) start()
runCatching { syncAssets() }
if (wallet.hasPerpetualsSupport && userConfig.isPerpetualEnabled().first()) {
runCatching { syncPerpetuals.syncPerpetuals() }
runCatching { syncPerpetualPositions.syncPerpetualPositions() }
}
}
}
scope.launchPerpetualSync(
session = sessionRepository.session(),
isPerpetualEnabled = userConfig.isPerpetualEnabled(),
syncPerpetuals = syncPerpetuals,
syncPerpetualPositions = syncPerpetualPositions,
)
}

fun start() {
Expand Down Expand Up @@ -139,3 +144,21 @@ class StreamObserverService(
private const val PING_INTERVAL_MS = 30_000L
}
}

internal fun CoroutineScope.launchPerpetualSync(
session: Flow<Session?>,
isPerpetualEnabled: Flow<Boolean>,
syncPerpetuals: SyncPerpetuals,
syncPerpetualPositions: SyncPerpetualPositions,
): Job = launch {
combine(session, isPerpetualEnabled) { current, enabled ->
val wallet = current?.wallet
if (wallet != null && wallet.hasPerpetualsSupport && enabled) wallet.id.id else null
}
.distinctUntilChanged()
.collectLatest { walletId ->
if (walletId == null) return@collectLatest
runCatching { syncPerpetuals.syncPerpetuals() }
runCatching { syncPerpetualPositions.syncPerpetualPositions() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import com.gemwallet.android.model.AuthRequest
import com.gemwallet.android.model.ConfirmParams
import com.gemwallet.android.model.ValueFormatter
import com.gemwallet.android.ui.R
import com.gemwallet.android.ui.components.perpetual.AutocloseSummaryRow
import com.gemwallet.android.ui.components.perpetual.PerpetualDetailsBottomSheet
import com.gemwallet.android.ui.components.perpetual.PerpetualDetailsSummaryItem
import com.gemwallet.android.ui.components.perpetual.title
Expand Down Expand Up @@ -317,6 +318,11 @@ private fun ConfirmDetailElementRow(
onClick = onClick,
listPosition = listPosition,
)
is ConfirmDetailElement.PerpetualModifyAutoclose -> AutocloseSummaryRow(
takeProfitText = item.takeProfitText,
stopLossText = item.stopLossText,
listPosition = listPosition,
)
}
}

Expand All @@ -340,6 +346,8 @@ private fun ConfirmDetailElementBottomSheet(
onDismiss = onDismiss,
)

is ConfirmDetailElement.PerpetualModifyAutoclose -> Unit

null -> Unit
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,9 @@ sealed interface ConfirmDetailElement {
data class PerpetualDetails(
val model: PerpetualConfirmDetailsUIModel,
) : ConfirmDetailElement

data class PerpetualModifyAutoclose(
val takeProfitText: String?,
val stopLossText: String?,
) : ConfirmDetailElement
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.gemwallet.android.features.confirm.models

import com.gemwallet.android.model.CurrencyFormatter
import com.wallet.core.primitives.Currency
import com.wallet.core.primitives.PerpetualModifyConfirmData
import com.wallet.core.primitives.PerpetualModifyPositionType

object PerpetualModifyAutocloseFactory {

private const val ClearedPlaceholder: String = "-"

fun create(data: PerpetualModifyConfirmData): ConfirmDetailElement.PerpetualModifyAutoclose? {
val tpsl = data.modifyTypes
.filterIsInstance<PerpetualModifyPositionType.Tpsl>()
.firstOrNull()?.content
val cancelOrderIds = data.modifyTypes
.filterIsInstance<PerpetualModifyPositionType.Cancel>()
.flatMap { it.content }
.map { it.orderId }
.toSet()
val takeProfitCanceled = data.takeProfitOrderId != null &&
data.takeProfitOrderId in cancelOrderIds
val stopLossCanceled = data.stopLossOrderId != null &&
data.stopLossOrderId in cancelOrderIds
val formatter = CurrencyFormatter(currency = Currency.USD)
val takeProfitText: String? = when {
tpsl?.takeProfit != null -> tpsl.takeProfit?.toDoubleOrNull()?.let(formatter::string)
takeProfitCanceled -> ClearedPlaceholder
else -> null
}
val stopLossText: String? = when {
tpsl?.stopLoss != null -> tpsl.stopLoss?.toDoubleOrNull()?.let(formatter::string)
stopLossCanceled -> ClearedPlaceholder
else -> null
}
if (takeProfitText == null && stopLossText == null) return null
return ConfirmDetailElement.PerpetualModifyAutoclose(takeProfitText, stopLossText)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ import com.gemwallet.android.ui.models.swap.SwapProviderUIModelFactory
import com.gemwallet.android.ui.models.actions.FinishConfirmAction
import com.gemwallet.android.domains.confirm.AmountUIModel
import com.gemwallet.android.features.confirm.models.ConfirmDetailElement
import com.gemwallet.android.features.confirm.models.PerpetualModifyAutocloseFactory
import com.gemwallet.android.domains.confirm.ConfirmError
import com.gemwallet.android.domains.confirm.ConfirmState
import com.gemwallet.android.domains.confirm.FeeUIModel
import com.wallet.core.primitives.AssetId
import com.wallet.core.primitives.Currency
import com.wallet.core.primitives.PerpetualType
import com.wallet.core.primitives.FeePriority
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -323,9 +325,10 @@ class ConfirmViewModel @Inject constructor(

private fun buildPerpetualDetailElement(
params: ConfirmParams.PerpetualParams?,
): ConfirmDetailElement.PerpetualDetails? {
val model = PerpetualConfirmDetailsUIModelFactory.create(params?.perpetualType ?: return null) ?: return null
return ConfirmDetailElement.PerpetualDetails(model)
): ConfirmDetailElement? = when (val type = params?.perpetualType) {
null -> null
is PerpetualType.Modify -> PerpetualModifyAutocloseFactory.create(type.content)
else -> PerpetualConfirmDetailsUIModelFactory.create(type)?.let(ConfirmDetailElement::PerpetualDetails)
}

private fun buildSwapDetailElement(
Expand Down
Loading
Loading