Skip to content
Merged
11 changes: 6 additions & 5 deletions app/src/main/java/to/bitkit/data/backup/VssBackupClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ class VssBackupClient @Inject constructor(
private val vssStoreIdProvider: VssStoreIdProvider,
private val keychain: Keychain,
) {
@Volatile
private var isSetup = CompletableDeferred<Unit>()
private val setupMutex = Mutex()

suspend fun setup(walletIndex: Int = 0): Result<Unit> = withContext(ioDispatcher) {
setupMutex.withLock {
val gate = isSetup
runCatching {
if (isSetup.isCompleted && !isSetup.isCancelled) {
runCatching { isSetup.await() }.onSuccess { return@runCatching }
}
if (gate.isCompleted && !gate.isCancelled) return@runCatching

Comment thread
ovitrif marked this conversation as resolved.
val mnemonic = keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name)
?: throw MnemonicNotAvailableException()
Expand All @@ -63,11 +63,12 @@ class VssBackupClient @Inject constructor(
passphrase = passphrase,
lnurlAuthServerUrl = lnurlAuthServerUrl,
)
isSetup.complete(Unit)
gate.complete(Unit)
Logger.info("VSS client setup with server: '$vssUrl'", context = TAG)
}
}.onFailure {
isSetup.completeExceptionally(it)
gate.completeExceptionally(it)
if (isSetup === gate) isSetup = CompletableDeferred()
Logger.error("VSS client setup error", it, context = TAG)
}
}
Expand Down
11 changes: 6 additions & 5 deletions app/src/main/java/to/bitkit/data/backup/VssBackupClientLdk.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,15 @@ class VssBackupClientLdk @Inject constructor(
)
}

@Volatile
private var isSetup = CompletableDeferred<Unit>()
private val setupMutex = Mutex()

suspend fun setup(walletIndex: Int = 0): Result<Unit> = withContext(ioDispatcher) {
setupMutex.withLock {
val gate = isSetup
runCatching {
if (isSetup.isCompleted && !isSetup.isCancelled) {
runCatching { isSetup.await() }.onSuccess { return@runCatching }
}
if (gate.isCompleted && !gate.isCancelled) return@runCatching

val mnemonic = keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name)
?: throw MnemonicNotAvailableException()
Expand All @@ -64,11 +64,12 @@ class VssBackupClientLdk @Inject constructor(
passphrase = passphrase,
lnurlAuthServerUrl = Env.lnurlAuthServerUrl,
)
isSetup.complete(Unit)
gate.complete(Unit)
Logger.info("VSS LDK client setup", context = TAG)
}
}.onFailure {
isSetup.completeExceptionally(it)
gate.completeExceptionally(it)
if (isSetup === gate) isSetup = CompletableDeferred()
Logger.error("VSS LDK client setup error", it, context = TAG)
}
}
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/to/bitkit/repositories/BackupRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ class BackupRepo @Inject constructor(
val isRestoring: StateFlow<Boolean> = _isRestoring.asStateFlow()

private val _isWiping = MutableStateFlow(false)
val isWiping: StateFlow<Boolean> = _isWiping.asStateFlow()

fun reset() {
stopObservingBackups()
Expand All @@ -140,6 +141,10 @@ class BackupRepo @Inject constructor(

fun startObservingBackups() {
if (isObserving) return
if (_isWiping.value) {
Logger.debug("Skipped observing backups while wiping", context = TAG)
return
}

isObserving = true
Logger.debug("Start observing backup statuses and data store changes", context = TAG)
Expand Down
76 changes: 44 additions & 32 deletions app/src/main/java/to/bitkit/repositories/LightningRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ class LightningRepo @Inject constructor(
private val _isRecoveryMode = MutableStateFlow(false)
val isRecoveryMode = _isRecoveryMode.asStateFlow()

@Volatile
private var isWiping = false

private val channelCache = ConcurrentHashMap<String, ChannelDetails>()
private val probeOutcomeCache = ConcurrentHashMap<PaymentId, ProbeOutcome>()
private val probeOutcomeSignal = MutableSharedFlow<ProbeOutcome>(extraBufferCapacity = 64)
Expand Down Expand Up @@ -341,6 +344,7 @@ class LightningRepo @Inject constructor(
var initialLifecycleState: NodeLifecycleState

val result = lifecycleMutex.withLock {
if (isWiping) return@withLock Result.failure(WipeInProgressError())
Comment thread
jvsena42 marked this conversation as resolved.
initialLifecycleState = _lightningState.value.nodeLifecycleState
if (initialLifecycleState.isRunningOrStarting()) {
return@withLock skipStartForRunningNode(
Expand Down Expand Up @@ -574,6 +578,8 @@ class LightningRepo @Inject constructor(

fun setRecoveryMode(enabled: Boolean) = _isRecoveryMode.update { enabled }

fun setWiping(enabled: Boolean) = run { isWiping = enabled }

suspend fun updateGeoBlockState() = withContext(bgDispatcher) {
_lightningState.update {
it.copy(isGeoBlocked = coreService.isGeoBlocked())
Expand Down Expand Up @@ -614,30 +620,32 @@ class LightningRepo @Inject constructor(
fun cancelPendingStop() = synchronized(pendingStopLock) { pendingStopJob.getAndSet(null)?.cancel() }

suspend fun stop(): Result<Unit> = withContext(bgDispatcher) {
lifecycleMutex.withLock {
if (_lightningState.value.nodeLifecycleState.isStoppedOrStopping()) {
lifecycleMutex.withLock { stopLocked() }
}

private suspend fun stopLocked(): Result<Unit> {
if (_lightningState.value.nodeLifecycleState.isStoppedOrStopping() && lightningService.node == null) {
clearProbeOutcomes()
return Result.success(Unit)
}

return runCatching {
withContext(NonCancellable) {
_lightningState.update { it.copy(nodeLifecycleState = NodeLifecycleState.Stopping) }
lightningService.stop()
clearProbeOutcomes()
return@withLock Result.success(Unit)
_lightningState.update { LightningState(nodeLifecycleState = NodeLifecycleState.Stopped) }
}

runCatching {
withContext(NonCancellable) {
_lightningState.update { it.copy(nodeLifecycleState = NodeLifecycleState.Stopping) }
lightningService.stop()
clearProbeOutcomes()
_lightningState.update { LightningState(nodeLifecycleState = NodeLifecycleState.Stopped) }
}
}.onFailure {
Logger.error("Node stop error", it, context = TAG)
// On failure, check actual node state and update accordingly
// If node is still running, revert to Running state to allow retry
if (lightningService.node != null && lightningService.status?.isRunning == true) {
Logger.warn("Stop failed but node is still running, reverting to Running state", context = TAG)
_lightningState.update { s -> s.copy(nodeLifecycleState = NodeLifecycleState.Running) }
} else {
// Node appears stopped, update state
_lightningState.update { LightningState(nodeLifecycleState = NodeLifecycleState.Stopped) }
}
}.onFailure {
Logger.error("Node stop error", it, context = TAG)
// On failure, check actual node state and update accordingly
// If node is still running, revert to Running state to allow retry
if (lightningService.node != null && lightningService.status?.isRunning == true) {
Logger.warn("Stop failed but node is still running, reverting to Running state", context = TAG)
_lightningState.update { s -> s.copy(nodeLifecycleState = NodeLifecycleState.Running) }
} else {
// Node appears stopped, update state
_lightningState.update { LightningState(nodeLifecycleState = NodeLifecycleState.Stopped) }
}
}
}
Expand Down Expand Up @@ -810,17 +818,19 @@ class LightningRepo @Inject constructor(

suspend fun wipeStorage(walletIndex: Int): Result<Unit> = withContext(bgDispatcher) {
Logger.debug("wipeStorage called, stopping node first", context = TAG)
stop().mapCatching {
Logger.debug("node stopped, calling wipeStorage", context = TAG)
lightningService.wipeStorage(walletIndex)
clearProbeOutcomes()
_lightningState.update {
LightningState(
nodeStatus = it.nodeStatus,
nodeLifecycleState = it.nodeLifecycleState,
)
lifecycleMutex.withLock {
stopLocked().mapCatching {
Logger.debug("node stopped, calling wipeStorage", context = TAG)
lightningService.wipeStorage(walletIndex)
clearProbeOutcomes()
_lightningState.update {
LightningState(
nodeStatus = it.nodeStatus,
nodeLifecycleState = it.nodeLifecycleState,
)
}
setRecoveryMode(false)
}
setRecoveryMode(false)
}.onFailure {
Logger.error("wipeStorage error", it, context = TAG)
}
Expand Down Expand Up @@ -2117,6 +2127,8 @@ private data class PaymentRoutingRefreshStatus(
}

class RecoveryModeError : AppError("App in recovery mode, skipping node start")

class WipeInProgressError : AppError("Wallet wipe in progress, refusing node start")
class NodeSetupError : AppError("Unknown node setup error")
class NodeStopTimeoutError : AppError("Timeout waiting for node to stop")
class NodeConfigNotAppliedError : AppError("Node already running, requested config was not applied")
Expand Down
5 changes: 4 additions & 1 deletion app/src/main/java/to/bitkit/repositories/WalletRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import to.bitkit.models.toDerivationPath
import to.bitkit.services.AddressDerivationInfo
import to.bitkit.services.CoreService
import to.bitkit.usecases.DeriveBalanceStateUseCase
import to.bitkit.usecases.WipeIncomplete
import to.bitkit.usecases.WipeWalletUseCase
import to.bitkit.utils.Bip21Utils
import to.bitkit.utils.Logger
Expand Down Expand Up @@ -477,7 +478,9 @@ class WalletRepo @Inject constructor(
walletIndex = walletIndex,
resetWalletState = ::resetState,
onSuccess = ::setWalletExistsState,
)
).onFailure {
if (it is WipeIncomplete) setWalletExistsState()
}
}

fun resetState() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package to.bitkit.ui.settings.backups

import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
Expand All @@ -23,6 +24,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import to.bitkit.R
import to.bitkit.ui.appViewModel
Expand All @@ -45,12 +47,17 @@ fun ResetAndRestoreScreen(
val app = appViewModel ?: return
val wallet = walletViewModel ?: return
var showDialog by remember { mutableStateOf(false) }
val isWiping by wallet.isWiping.collectAsStateWithLifecycle()

Content(
showConfirmDialog = showDialog,
isWiping = isWiping,
onClickBackup = { app.showSheet(Sheet.Backup()) },
onClickReset = { showDialog = true },
onResetConfirm = { wallet.wipeWallet() },
onResetConfirm = {
showDialog = false
wallet.wipeWallet()
},
onResetDismiss = { showDialog = false },
onBack = { navController.popBackStack() },
)
Expand All @@ -59,16 +66,19 @@ fun ResetAndRestoreScreen(
@Composable
private fun Content(
showConfirmDialog: Boolean,
isWiping: Boolean,
onClickBackup: () -> Unit,
onClickReset: () -> Unit,
onResetConfirm: () -> Unit,
onResetDismiss: () -> Unit,
onBack: () -> Unit,
) {
BackHandler(enabled = isWiping) {}

ScreenColumn {
AppTopBar(
titleText = stringResource(R.string.security__reset_title),
onBackClick = onBack,
onBackClick = if (isWiping) null else onBack,
actions = { DrawerNavIcon() },
)
Spacer(Modifier.height(32.dp))
Expand Down Expand Up @@ -101,13 +111,15 @@ private fun Content(
SecondaryButton(
text = stringResource(R.string.security__reset_button_backup),
onClick = onClickBackup,
enabled = !isWiping,
modifier = Modifier
.weight(1f)
.testTag(ResetAndRestoreTestTags.BACKUP_BUTTON)
)
PrimaryButton(
text = stringResource(R.string.security__reset_button_reset),
onClick = onClickReset,
isLoading = isWiping,
modifier = Modifier
.weight(1f)
.testTag(ResetAndRestoreTestTags.RESET_BUTTON)
Expand Down Expand Up @@ -143,6 +155,7 @@ private fun Preview() {
AppThemeSurface {
Content(
showConfirmDialog = false,
isWiping = false,
onClickBackup = {},
onClickReset = {},
onResetConfirm = {},
Expand All @@ -158,6 +171,7 @@ private fun PreviewDialog() {
AppThemeSurface {
Content(
showConfirmDialog = true,
isWiping = false,
onClickBackup = {},
onClickReset = {},
onResetConfirm = {},
Expand Down
Loading
Loading