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
4 changes: 2 additions & 2 deletions app/src/main/java/to/bitkit/models/AddressType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ fun String.toAddressType(): AddressType? = when (this) {
val ALL_ADDRESS_TYPE_STRINGS = listOf("legacy", "nestedSegwit", "nativeSegwit", "taproot")

fun String.addressTypeFromAddress(): String? = when {
startsWith("bc1p") || startsWith("tb1p") -> "taproot"
startsWith("bc1") || startsWith("tb1") -> "nativeSegwit"
startsWith("bc1p") || startsWith("tb1p") || startsWith("bcrt1p") -> "taproot"
startsWith("bc1") || startsWith("tb1") || startsWith("bcrt1") -> "nativeSegwit"
startsWith("3") || startsWith("2") -> "nestedSegwit"
startsWith("1") || startsWith("m") || startsWith("n") -> "legacy"
else -> null
Expand Down
51 changes: 22 additions & 29 deletions app/src/main/java/to/bitkit/repositories/LightningRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,7 @@ class LightningRepo @Inject constructor(
val previousSettings = settingsStore.data.first()
val oldSelected = previousSettings.selectedAddressType
val oldMonitored = previousSettings.addressTypesToMonitor
val addressType = selectedType.toAddressType() ?: AddressType.P2WPKH

suspend fun rollback() =
settingsStore.update { it.copy(selectedAddressType = oldSelected, addressTypesToMonitor = oldMonitored) }
Expand All @@ -684,7 +685,8 @@ class LightningRepo @Inject constructor(
settingsStore.update {
it.copy(selectedAddressType = selectedType, addressTypesToMonitor = monitoredTypes)
}
restartNodeOrRollback(onRollback = { rollback() })
lightningService.setPrimaryAddressType(addressType)
sync().onFailure { Logger.warn("Sync after address type change failed", it, context = TAG) }
Unit
}.onFailure {
rollback()
Expand Down Expand Up @@ -717,7 +719,12 @@ class LightningRepo @Inject constructor(

runCatching {
settingsStore.update { it.copy(addressTypesToMonitor = newMonitored) }
restartNodeOrRollback(onRollback = { rollback() })
if (enabled) {
lightningService.addAddressTypeToMonitor(addressType)
} else {
lightningService.removeAddressTypeFromMonitor(addressType)
}
sync().onFailure { Logger.warn("Sync after monitoring change failed", it, context = TAG) }
Unit
}.onFailure {
rollback()
Expand Down Expand Up @@ -749,24 +756,6 @@ class LightningRepo @Inject constructor(
return null
}

@Suppress("ThrowsCount")
private suspend fun restartNodeOrRollback(onRollback: suspend () -> Unit) {
waitForNodeToStop().onFailure {
onRollback()
throw it
}
stop().onFailure {
onRollback()
throw it
}
start(shouldRetry = false).onFailure {
onRollback()
restartWithPreviousConfig()
throw it
}
sync().onFailure { Logger.warn("Sync after address type change failed", it, context = TAG) }
}

fun isChangingAddressType(): Boolean = isChangingAddressType.get()

suspend fun pruneEmptyAddressTypesAfterRestore(): Result<Unit> = withContext(bgDispatcher) {
Expand All @@ -791,12 +780,13 @@ class LightningRepo @Inject constructor(

val newMonitored = monitored.filter { it !in toRemove }
settingsStore.update { it.copy(addressTypesToMonitor = newMonitored) }
stop().onFailure { return@withContext Result.failure(it) }
start(shouldRetry = false).onFailure {
settingsStore.update { it.copy(addressTypesToMonitor = monitored) }
return@withContext Result.failure(it)
for (typeStr in toRemove) {
val type = typeStr.toAddressType() ?: continue
runCatching { lightningService.removeAddressTypeFromMonitor(type) }.onFailure {
Logger.error("Failed to remove address type $typeStr from monitor", it, context = TAG)
}
}
sync().onFailure { Logger.warn("Initial sync after prune failed", it, context = TAG) }
sync().onFailure { Logger.warn("Sync after prune failed", it, context = TAG) }
Result.success(Unit)
}

Expand Down Expand Up @@ -983,12 +973,15 @@ class LightningRepo @Inject constructor(
val transactionSpeed = speed ?: settingsStore.data.first().defaultTransactionSpeed
val satsPerVByte = getFeeRateForSpeed(transactionSpeed, feeRates).getOrThrow()

// use passed utxos if specified, otherwise run auto coin select if enabled
val finalUtxosToSpend = utxosToSpend ?: determineUtxosToSpend(sats, satsPerVByte)
// transfer send-all: skip UTXO selection to avoid LDK buffer; else use passed or auto-selected
val utxosForSend = when {
isTransfer && isMaxAmount -> null
else -> utxosToSpend ?: determineUtxosToSpend(sats, satsPerVByte)
}

Logger.debug("UTXOs selected to spend: $finalUtxosToSpend", context = TAG)
Logger.debug("UTXOs selected to spend: $utxosForSend", context = TAG)

val txId = lightningService.send(address, sats, satsPerVByte, finalUtxosToSpend, isMaxAmount)
val txId = lightningService.send(address, sats, satsPerVByte, utxosForSend, isMaxAmount)

val preActivityMetadata = PreActivityMetadata(
paymentId = txId,
Expand Down
19 changes: 19 additions & 0 deletions app/src/main/java/to/bitkit/services/LightningService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,25 @@ class LightningService @Inject constructor(
n.getBalanceForAddressType(addressType.toLdkAddressType())
}

suspend fun setPrimaryAddressType(addressType: AddressType) = ServiceQueue.LDK.background {
val n = node ?: throw ServiceError.NodeNotSetup()
val mnemonic = keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name) ?: throw ServiceError.MnemonicNotFound()
val passphrase = keychain.loadString(Keychain.Key.BIP39_PASSPHRASE.name)
n.setPrimaryAddressTypeWithMnemonic(addressType.toLdkAddressType(), mnemonic, passphrase)
}

suspend fun addAddressTypeToMonitor(addressType: AddressType) = ServiceQueue.LDK.background {
val n = node ?: throw ServiceError.NodeNotSetup()
val mnemonic = keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name) ?: throw ServiceError.MnemonicNotFound()
val passphrase = keychain.loadString(Keychain.Key.BIP39_PASSPHRASE.name)
n.addAddressTypeToMonitorWithMnemonic(addressType.toLdkAddressType(), mnemonic, passphrase)
}

suspend fun removeAddressTypeFromMonitor(addressType: AddressType) = ServiceQueue.LDK.background {
val n = node ?: throw ServiceError.NodeNotSetup()
n.removeAddressTypeFromMonitor(addressType.toLdkAddressType())
}

private fun AddressType.toLdkAddressType(): LdkAddressType = when (this) {
AddressType.P2PKH -> LdkAddressType.LEGACY
AddressType.P2SH -> LdkAddressType.NESTED_SEGWIT
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,15 @@
package to.bitkit.ui.settings.advanced

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
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
Expand All @@ -31,21 +20,15 @@ import com.synonym.bitkitcore.AddressType
import to.bitkit.R
import to.bitkit.models.addressTypeInfo
import to.bitkit.models.toSettingsString
import to.bitkit.ui.components.BodyM
import to.bitkit.ui.components.Display
import to.bitkit.ui.components.FillHeight
import to.bitkit.ui.components.VerticalSpacer
import to.bitkit.ui.components.settings.SectionHeader
import to.bitkit.ui.components.settings.SettingsButtonRow
import to.bitkit.ui.components.settings.SettingsButtonValue
import to.bitkit.ui.components.settings.SettingsSwitchRow
import to.bitkit.ui.navigateToHome
import to.bitkit.ui.scaffold.AppTopBar
import to.bitkit.ui.scaffold.DrawerNavIcon
import to.bitkit.ui.scaffold.ScreenColumn
import to.bitkit.ui.theme.AppThemeSurface
import to.bitkit.ui.theme.Colors
import to.bitkit.ui.utils.withAccent

private val ADDRESS_TYPES = listOf(
AddressType.P2PKH,
Expand All @@ -69,12 +52,6 @@ fun AddressTypePreferenceScreen(
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()

LaunchedEffect(Unit) {
viewModel.navigateToHome.collect {
navController.navigateToHome()
}
}

Content(
uiState = uiState,
onBack = { navController.popBackStack() },
Expand All @@ -90,145 +67,58 @@ private fun Content(
onSelectAddressType: (AddressType) -> Unit = {},
onSetMonitoring: (AddressType, Boolean) -> Unit = { _, _ -> },
) {
Box(
content = {
ScreenColumn {
AppTopBar(
titleText = stringResource(R.string.settings__addr_type__title),
onBackClick = onBack,
actions = { DrawerNavIcon() },
)
Column(
content = {
SectionHeader(title = stringResource(R.string.settings__addr_type__primary))

ADDRESS_TYPES.forEach { type ->
val info = type.addressTypeInfo()
SettingsButtonRow(
title = "${info.shortName} ${info.example}",
subtitle = info.description,
value = SettingsButtonValue.BooleanValue(uiState.selectedAddressType == type),
onClick = { onSelectAddressType(type) },
modifier = Modifier.testTag(type.toAddressTypeE2eId()),
)
}

if (uiState.showMonitoredTypes) {
SectionHeader(title = stringResource(R.string.settings__addr_type__monitoring))
ScreenColumn {
AppTopBar(
titleText = stringResource(R.string.settings__addr_type__title),
onBackClick = onBack,
actions = { DrawerNavIcon() },
)
Column(
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxSize()
.verticalScroll(rememberScrollState())
.testTag("AddressTypePreference"),
) {
SectionHeader(title = stringResource(R.string.settings__addr_type__primary))

ADDRESS_TYPES.forEach { type ->
val info = type.addressTypeInfo()
val isMonitored = type.toSettingsString() in uiState.monitoredTypes
val isSelectedType = uiState.selectedAddressType == type
Column(
content = {
SettingsSwitchRow(
title = "${info.shortName} ${info.shortExample}",
subtitle = if (isSelectedType) {
stringResource(R.string.settings__adv__addr_type_currently_selected)
} else {
null
},
isChecked = isMonitored,
onClick = { if (!isSelectedType) onSetMonitoring(type, !isMonitored) },
modifier = Modifier.testTag("MonitorToggle-${type.toAddressTypeE2eId()}"),
)
},
modifier = Modifier.alpha(if (isSelectedType) 0.5f else 1f),
)
}
}
},
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxSize()
.verticalScroll(rememberScrollState())
.testTag("AddressTypePreference"),
)
}
if (uiState.isLoading) {
Box(
content = {
AddressTypeLoadingContent(
targetAddressType = uiState.loadingAddressType,
isMonitoringChange = uiState.isMonitoringChange,
)
},
modifier = Modifier
.fillMaxSize()
.background(Colors.Black),
ADDRESS_TYPES.forEach { type ->
val info = type.addressTypeInfo()
SettingsButtonRow(
title = "${info.shortName} ${info.example}",
subtitle = info.description,
value = SettingsButtonValue.BooleanValue(uiState.selectedAddressType == type),
onClick = { onSelectAddressType(type) },
modifier = Modifier.testTag(type.toAddressTypeE2eId()),
)
}
},
modifier = Modifier.fillMaxSize(),
)
}

@Composable
private fun AddressTypeLoadingContent(
targetAddressType: AddressType?,
isMonitoringChange: Boolean,
modifier: Modifier = Modifier,
) {
val navTitle = if (isMonitoringChange) {
stringResource(R.string.settings__addr_type__loading_nav_monitoring)
} else {
stringResource(R.string.settings__addr_type__loading_nav_address)
}
val headline = if (targetAddressType != null && !isMonitoringChange) {
stringResource(R.string.settings__addr_type__loading_headline)
.replace("{type}", "<accent>${targetAddressType.addressTypeInfo().shortName}</accent>")
} else {
stringResource(R.string.settings__addr_type__loading_updating)
}
val description = stringResource(R.string.settings__addr_type__loading_desc)
if (uiState.showMonitoredTypes) {
SectionHeader(title = stringResource(R.string.settings__addr_type__monitoring))

Column(
content = {
AppTopBar(
titleText = navTitle,
onBackClick = null,
actions = {},
)
Column(
content = {
FillHeight()
Image(
painter = painterResource(R.drawable.wallet),
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier.fillMaxWidth(),
)
FillHeight()
Column(
verticalArrangement = Arrangement.spacedBy(14.dp),
content = {
Display(
text = headline.withAccent(accentColor = Colors.Brand),
)
BodyM(text = description, color = Colors.White64)
ADDRESS_TYPES.forEach { type ->
val info = type.addressTypeInfo()
val isMonitored = type.toSettingsString() in uiState.monitoredTypes
val isSelectedType = uiState.selectedAddressType == type
SettingsSwitchRow(
title = "${info.shortName} ${info.shortExample}",
subtitle = if (isSelectedType) {
stringResource(R.string.settings__adv__addr_type_currently_selected)
} else {
null
},
modifier = Modifier.fillMaxWidth(),
)
VerticalSpacer(32.dp)
CircularProgressIndicator(
color = Colors.White32,
strokeWidth = 3.dp,
isChecked = isMonitored,
onClick = { if (!isSelectedType) onSetMonitoring(type, !isMonitored) },
modifier = Modifier
.align(Alignment.CenterHorizontally)
.size(32.dp),
.alpha(if (isSelectedType) 0.5f else 1f)
.testTag("MonitorToggle-${type.toAddressTypeE2eId()}"),
)
VerticalSpacer(32.dp)
},
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
)
},
modifier = modifier
.fillMaxSize()
.padding(16.dp),
)
}
}

VerticalSpacer(16.dp)
}
}
}

@Preview
Expand Down
Loading
Loading