diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index b538ec580..68ace4657 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -1,7 +1,6 @@ object Versions { const val lightningKmp = "1.8.4" const val secp256k1 = "0.14.0" - const val torMobile = "0.2.0" const val kotlin = "1.9.22" diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/AppView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/AppView.kt index 9a25ccdc1..39486b2c1 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/AppView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/AppView.kt @@ -81,7 +81,7 @@ import fr.acinq.phoenix.android.services.NodeServiceState import fr.acinq.phoenix.android.settings.AboutView import fr.acinq.phoenix.android.settings.AppAccessSettings import fr.acinq.phoenix.android.settings.DisplayPrefsView -import fr.acinq.phoenix.android.settings.ElectrumView +import fr.acinq.phoenix.android.settings.electrum.ElectrumView import fr.acinq.phoenix.android.settings.ExperimentalView import fr.acinq.phoenix.android.settings.ForceCloseView import fr.acinq.phoenix.android.settings.LogsView @@ -366,7 +366,7 @@ fun AppView( ElectrumView() } composable(Screen.TorConfig.route) { - TorConfigView() + TorConfigView(appViewModel = appVM, onBackClick = { navController.popBackStack() }, onBusinessTeardown = { navController.popToHome() }) } composable(Screen.Channels.route) { ChannelsView( diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/Dialogs.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/Dialogs.kt index 23166c70b..e6bd55f89 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/Dialogs.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/Dialogs.kt @@ -64,8 +64,8 @@ fun Dialog( title: String? = null, properties: DialogProperties = DialogProperties(usePlatformDefaultWidth = false), isScrollable: Boolean = true, - buttonsTopMargin: Dp = 24.dp, - buttons: (@Composable RowScope.() -> Unit)? = { Button(onClick = onDismiss, text = stringResource(id = R.string.btn_ok), padding = PaddingValues(16.dp)) }, + buttonsTopMargin: Dp = 20.dp, + buttons: (@Composable RowScope.() -> Unit)? = { Button(onClick = onDismiss, text = stringResource(id = R.string.btn_ok), padding = PaddingValues(16.dp), shape = RoundedCornerShape(16.dp)) }, content: @Composable ColumnScope.() -> Unit, ) { androidx.compose.ui.window.Dialog(onDismissRequest = onDismiss, properties = properties) { @@ -79,10 +79,7 @@ fun Dialog( // buttons if (buttons != null) { Spacer(Modifier.height(buttonsTopMargin)) - Row( - modifier = Modifier - .align(Alignment.End) - ) { + Row(modifier = Modifier.align(Alignment.End).padding(8.dp)) { buttons() } } diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/ProgressView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/ProgressView.kt index 1cce8d39a..7b1049762 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/ProgressView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/ProgressView.kt @@ -36,9 +36,11 @@ fun ProgressView( progressCircleSize: Dp = 20.dp, progressCircleWidth: Dp = 2.dp, space: Dp = 8.dp, + horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, ) { Row( modifier.padding(padding), + horizontalArrangement = horizontalArrangement, ) { CircularProgressIndicator(Modifier.size(progressCircleSize), strokeWidth = progressCircleWidth) Spacer(Modifier.width(space)) diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/ConnectionDialog.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/ConnectionDialog.kt index 134148984..2dfd30054 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/ConnectionDialog.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/ConnectionDialog.kt @@ -18,8 +18,10 @@ package fr.acinq.phoenix.android.home import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -30,24 +32,27 @@ import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import fr.acinq.lightning.utils.Connection import fr.acinq.phoenix.android.R +import fr.acinq.phoenix.android.components.Card import fr.acinq.phoenix.android.components.Dialog import fr.acinq.phoenix.android.components.HSeparator import fr.acinq.phoenix.android.components.TextWithIcon import fr.acinq.phoenix.android.userPrefs import fr.acinq.phoenix.android.utils.extensions.isBadCertificate import fr.acinq.phoenix.android.utils.monoTypo +import fr.acinq.phoenix.android.utils.mutedBgColor import fr.acinq.phoenix.android.utils.negativeColor import fr.acinq.phoenix.android.utils.orange import fr.acinq.phoenix.android.utils.positiveColor import fr.acinq.phoenix.managers.Connections +import fr.acinq.phoenix.utils.extensions.isOnion @Composable @@ -66,45 +71,29 @@ fun ConnectionDialog( modifier = Modifier.padding(top = 16.dp, start = 24.dp, end = 24.dp) ) } else { - if (connections.electrum != Connection.ESTABLISHED || connections.peer != Connection.ESTABLISHED) { + val hasConnectionIssues = connections.electrum != Connection.ESTABLISHED || connections.peer != Connection.ESTABLISHED + if (hasConnectionIssues) { Text(text = stringResource(id = R.string.conndialog_summary_not_ok), Modifier.padding(horizontal = 24.dp)) } - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(16.dp)) HSeparator() - - val isTorEnabled = userPrefs.getIsTorEnabled.collectAsState(initial = null).value - if (isTorEnabled != null && isTorEnabled) { - ConnectionDialogLine(label = stringResource(id = R.string.conndialog_tor), connection = connections.tor, onClick = onTorClick) - HSeparator() - } - ConnectionDialogLine(label = stringResource(id = R.string.conndialog_electrum), connection = connections.electrum, onClick = onElectrumClick) { when (val connection = connections.electrum) { Connection.ESTABLISHING -> { Text(text = stringResource(R.string.conndialog_connecting), style = monoTypo) } Connection.ESTABLISHED -> { - Column { - Text(text = stringResource(R.string.conndialog_connected), style = monoTypo) - if (electrumBlockheight < 795_000) { // FIXME use a dynamic blockheight - TextWithIcon( - text = stringResource(id = R.string.conndialog_connected_electrum_behind, electrumBlockheight), - textStyle = MaterialTheme.typography.body1.copy(fontSize = 14.sp), - icon = R.drawable.ic_alert_triangle, - iconTint = negativeColor - ) - } - } + Text(text = stringResource(R.string.conndialog_connected), style = monoTypo) } else -> { - Text( - text = if (connection is Connection.CLOSED && connection.isBadCertificate()) { - stringResource(R.string.conndialog_closed_bad_cert) - } else { - stringResource(R.string.conndialog_closed) - }, - style = monoTypo - ) + val customElectrumServer by userPrefs.getElectrumServer.collectAsState(initial = null) + if (customElectrumServer?.isOnion == false) { + TextWithIcon(text = stringResource(R.string.conndialog_electrum_not_onion), textStyle = monoTypo, icon = R.drawable.ic_alert_triangle, iconTint = negativeColor) + } else if (connection is Connection.CLOSED && connection.isBadCertificate()) { + TextWithIcon(text = stringResource(R.string.conndialog_closed_bad_cert), textStyle = monoTypo, icon = R.drawable.ic_alert_triangle, iconTint = negativeColor) + } else { + Text(text = stringResource(R.string.conndialog_closed), style = monoTypo) + } } } } @@ -112,6 +101,15 @@ fun ConnectionDialog( ConnectionDialogLine(label = stringResource(id = R.string.conndialog_lightning), connection = connections.peer) HSeparator() Spacer(Modifier.height(16.dp)) + + val isTorEnabled = userPrefs.getIsTorEnabled.collectAsState(initial = null).value + if (hasConnectionIssues && isTorEnabled == true) { + Card(backgroundColor = mutedBgColor, modifier = Modifier.fillMaxWidth(), internalPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp), onClick = onTorClick) { + TextWithIcon(text = stringResource(id = R.string.conndialog_tor_disclaimer_title), icon = R.drawable.ic_tor_shield, textStyle = MaterialTheme.typography.body2) + Spacer(modifier = Modifier.height(4.dp)) + Text(text = stringResource(id = R.string.conndialog_tor_disclaimer_body)) + } + } } } } @@ -147,7 +145,7 @@ private fun ConnectionDialogLine( .then( if (onClick != null) Modifier.clickable(role = Role.Button, onClickLabel = stringResource(id = R.string.conndialog_accessibility_desc, label), onClick = onClick) else Modifier ) - .padding(vertical = 12.dp, horizontal = 24.dp), + .padding(vertical = 16.dp, horizontal = 24.dp), verticalAlignment = Alignment.CenterVertically ) { Surface( @@ -161,6 +159,7 @@ private fun ConnectionDialogLine( ) {} Spacer(modifier = Modifier.width(16.dp)) Text(text = label, modifier = Modifier.weight(1.0f)) + Spacer(modifier = Modifier.width(24.dp)) content() } } \ No newline at end of file diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/HomeBalance.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/HomeBalance.kt index 40ed2f48f..3bc82f15a 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/HomeBalance.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/HomeBalance.kt @@ -80,7 +80,7 @@ fun HomeBalance( balanceDisplayMode: HomeAmountDisplayMode, ) { if (balance == null) { - ProgressView(modifier = modifier, text = stringResource(id = R.string.home__balance_loading)) + ProgressView(modifier = modifier, text = stringResource(id = R.string.home_balance_loading)) } else { val isAmountRedacted = balanceDisplayMode == HomeAmountDisplayMode.REDACTED Column( @@ -136,7 +136,7 @@ private fun OnChainBalance( ) { TextWithIcon( text = if (balanceDisplayMode == HomeAmountDisplayMode.REDACTED) "****" else { - stringResource(id = R.string.home__onchain_incoming, availableOnchainBalance.toPrettyString(preferredAmountUnit, fiatRate, withUnit = true)) + stringResource(id = R.string.home_onchain_incoming, availableOnchainBalance.toPrettyString(preferredAmountUnit, fiatRate, withUnit = true)) }, textStyle = MaterialTheme.typography.caption, icon = R.drawable.ic_chain, diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/HomePayments.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/HomePayments.kt index 8812f744a..526fb45a7 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/HomePayments.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/HomePayments.kt @@ -30,7 +30,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import fr.acinq.lightning.utils.sat import fr.acinq.phoenix.android.PaymentRowState import fr.acinq.phoenix.android.PaymentsViewModel import fr.acinq.phoenix.android.R @@ -57,7 +56,7 @@ fun ColumnScope.PaymentsList( Column(modifier = modifier.weight(1f, fill = true), horizontalAlignment = Alignment.CenterHorizontally) { if (payments.isEmpty()) { Text( - text = stringResource(id = R.string.home__payments_none), + text = stringResource(id = R.string.home_payments_none), style = MaterialTheme.typography.caption.copy(textAlign = TextAlign.Center, fontSize = 14.sp), modifier = Modifier .padding(horizontal = 32.dp) @@ -87,7 +86,7 @@ private fun ColumnScope.LatestPaymentsList( ) { val morePaymentsButton: @Composable () -> Unit = { FilledButton( - text = stringResource(id = R.string.home__payments_more_button), + text = stringResource(id = R.string.home_payments_more_button), icon = R.drawable.ic_chevron_down, iconTint = MaterialTheme.typography.caption.color, onClick = onPaymentsHistoryClick, diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/HomeTopAndBottom.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/HomeTopAndBottom.kt index cf1cc1872..a29d785f5 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/HomeTopAndBottom.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/HomeTopAndBottom.kt @@ -25,6 +25,7 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -48,11 +49,13 @@ import fr.acinq.phoenix.android.components.FilledButton import fr.acinq.phoenix.android.components.TextWithIcon import fr.acinq.phoenix.android.components.VSeparator import fr.acinq.phoenix.android.components.openLink +import fr.acinq.phoenix.android.userPrefs import fr.acinq.phoenix.android.utils.extensions.isBadCertificate import fr.acinq.phoenix.android.utils.negativeColor import fr.acinq.phoenix.android.utils.positiveColor import fr.acinq.phoenix.android.utils.warningColor import fr.acinq.phoenix.managers.Connections +import fr.acinq.phoenix.utils.extensions.isOnion @Composable fun TopBar( @@ -61,7 +64,6 @@ fun TopBar( connections: Connections, electrumBlockheight: Int, onTorClick: () -> Unit, - isTorEnabled: Boolean?, isFCMUnavailable: Boolean, isPowerSaverMode: Boolean, inFlightPaymentsCount: Int, @@ -82,7 +84,6 @@ fun TopBar( connections = connections, electrumBlockheight = electrumBlockheight, onTorClick = onTorClick, - isTorEnabled = isTorEnabled, ) if (inFlightPaymentsCount > 0) { @@ -91,7 +92,6 @@ fun TopBar( BackgroundRestrictionBadge( isFCMUnavailable = isFCMUnavailable, - isTorEnabled = isTorEnabled == true, isPowerSaverMode = isPowerSaverMode ) @@ -111,7 +111,7 @@ fun TopBar( } TopBadgeButton( - text = stringResource(R.string.home__faq_button), + text = stringResource(R.string.home_faq_button), icon = R.drawable.ic_help_circle, iconTint = MaterialTheme.colors.onSurface, onClick = { openLink(context, "https://phoenix.acinq.co/faq") }, @@ -125,8 +125,8 @@ private fun ConnectionBadge( connections: Connections, electrumBlockheight: Int, onTorClick: () -> Unit, - isTorEnabled: Boolean?, ) { + val torEnabled = userPrefs.getIsTorEnabled.collectAsState(initial = null) val connectionsTransition = rememberInfiniteTransition(label = "animateConnectionsBadge") val connectionsButtonAlpha by connectionsTransition.animateFloat( label = "animateConnectionsBadge", @@ -138,36 +138,62 @@ private fun ConnectionBadge( ), ) - if (connections.electrum !is Connection.ESTABLISHED || connections.peer !is Connection.ESTABLISHED) { - val electrumConnection = connections.electrum - val isBadElectrumCert = electrumConnection is Connection.CLOSED && electrumConnection.isBadCertificate() - TopBadgeButton( - text = stringResource(id = if (isBadElectrumCert) R.string.home__connection__bad_cert else R.string.home__connection__connecting), - icon = if (isBadElectrumCert) R.drawable.ic_alert_triangle else R.drawable.ic_connection_lost, - iconTint = if (isBadElectrumCert) negativeColor else MaterialTheme.colors.onSurface, - onClick = onConnectionsStateButtonClick, - modifier = Modifier.alpha(connectionsButtonAlpha) - ) - } else if (electrumBlockheight < 795_000) { - // FIXME use a dynamic blockheight ^ - TopBadgeButton( - text = stringResource(id = R.string.home__connection__electrum_late), - icon = R.drawable.ic_alert_triangle, - iconTint = warningColor, - onClick = onConnectionsStateButtonClick, - modifier = Modifier.alpha(connectionsButtonAlpha) - ) - } else if (isTorEnabled == true) { - if (connections.tor is Connection.ESTABLISHED) { + when { + connections.electrum !is Connection.ESTABLISHED -> { + val electrumConnection = connections.electrum + val isBadElectrumCert = electrumConnection is Connection.CLOSED && electrumConnection.isBadCertificate() + val customElectrumServer by userPrefs.getElectrumServer.collectAsState(initial = null) + + when { + isBadElectrumCert -> TopBadgeButton( + text = stringResource(id = R.string.home_connection_bad_cert), + icon = R.drawable.ic_alert_triangle, + iconTint = negativeColor, + onClick = onConnectionsStateButtonClick, + modifier = Modifier.alpha(connectionsButtonAlpha) + ) + torEnabled.value == true && customElectrumServer?.isOnion == false -> TopBadgeButton( + text = stringResource(id = R.string.home_connection_onion), + icon = R.drawable.ic_tor_shield, + iconTint = negativeColor, + onClick = onConnectionsStateButtonClick, + modifier = Modifier.alpha(connectionsButtonAlpha) + ) + else -> TopBadgeButton( + text = stringResource(id = R.string.home_connection_connecting), + icon = R.drawable.ic_connection_lost, + iconTint = MaterialTheme.colors.onSurface, + onClick = onConnectionsStateButtonClick, + modifier = Modifier.alpha(connectionsButtonAlpha) + ) + } + } + connections.peer !is Connection.ESTABLISHED -> { + TopBadgeButton( + text = stringResource(id = R.string.home_connection_connecting), + icon = R.drawable.ic_connection_lost, + iconTint = MaterialTheme.colors.onSurface, + onClick = onConnectionsStateButtonClick, + modifier = Modifier.alpha(connectionsButtonAlpha) + ) + } +// TODO: display a warning for desynced Electrum servers +// electrumBlockheight < XXX -> TopBadgeButton( +// text = stringResource(id = R.string.home_connection_electrum_late), +// icon = R.drawable.ic_alert_triangle, +// iconTint = warningColor, +// onClick = onConnectionsStateButtonClick, +// modifier = Modifier.alpha(connectionsButtonAlpha) +// ) + torEnabled.value == true -> { TopBadgeButton( - text = stringResource(id = R.string.home__connection__tor_active), + text = stringResource(id = R.string.home_connection_tor_active), icon = R.drawable.ic_tor_shield_ok, iconTint = positiveColor, onClick = onTorClick, ) } } - Spacer(modifier = Modifier.width(4.dp)) } @@ -195,13 +221,11 @@ private fun TopBadgeButton( @Composable private fun BackgroundRestrictionBadge( - isTorEnabled: Boolean, isPowerSaverMode: Boolean, isFCMUnavailable: Boolean, ) { - if (isTorEnabled || isPowerSaverMode || isFCMUnavailable) { + if (isPowerSaverMode || isFCMUnavailable) { var showDialog by remember { mutableStateOf(false) } - TopBadgeButton( text = null, icon = R.drawable.ic_alert_triangle, @@ -224,9 +248,6 @@ private fun BackgroundRestrictionBadge( Spacer(modifier = Modifier.height(8.dp)) Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - if (isTorEnabled) { - TextWithIcon(text = stringResource(id = R.string.home_background_restriction_tor), icon = R.drawable.ic_tor_shield_ok) - } if (isPowerSaverMode) { TextWithIcon(text = stringResource(id = R.string.home_background_restriction_powersaver), icon = R.drawable.ic_battery_charging) } diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/HomeView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/HomeView.kt index 0ffa9578a..c3cb60ed5 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/HomeView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/HomeView.kt @@ -90,7 +90,6 @@ fun HomeView( val isPowerSaverModeOn = noticesViewModel.isPowerSaverModeOn val fcmToken by internalData.getFcmToken.collectAsState(initial = "") val isFCMAvailable = remember { FCMHelper.isFCMAvailable(context) } - val torEnabledState = userPrefs.getIsTorEnabled.collectAsState(initial = null) val balanceDisplayMode by userPrefs.getHomeAmountDisplayMode.collectAsState(initial = HomeAmountDisplayMode.REDACTED) val connections by business.connectionsManager.connections.collectAsState() @@ -229,7 +228,6 @@ fun HomeView( connections = connections, electrumBlockheight = electrumMessages?.blockHeight ?: 0, inFlightPaymentsCount = inFlightPaymentsCount, - isTorEnabled = torEnabledState.value, onTorClick = onTorClick, isFCMUnavailable = fcmToken == null || !isFCMAvailable, isPowerSaverMode = isPowerSaverModeOn, diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/receive/ReceiveBaseView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/receive/ReceiveBaseView.kt index 9b4a67754..1cb50aa65 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/receive/ReceiveBaseView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/receive/ReceiveBaseView.kt @@ -393,8 +393,6 @@ fun TorWarning() { Text(text = stringResource(id = R.string.receive_tor_warning_title), style = MaterialTheme.typography.h4) Spacer(modifier = Modifier.height(12.dp)) Text(text = stringResource(id = R.string.receive_tor_warning_dialog_content_1)) - Spacer(modifier = Modifier.height(12.dp)) - Text(text = stringResource(id = R.string.receive_tor_warning_dialog_content_2)) } } } diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/ElectrumView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/ElectrumView.kt deleted file mode 100644 index e7dff8ef8..000000000 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/ElectrumView.kt +++ /dev/null @@ -1,347 +0,0 @@ -/* - * Copyright 2021 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - -package fr.acinq.phoenix.android.settings - - -import android.util.Base64 -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.lifecycle.viewmodel.compose.viewModel -import fr.acinq.lightning.io.TcpSocket -import fr.acinq.lightning.utils.Connection -import fr.acinq.lightning.utils.ServerAddress -import fr.acinq.phoenix.android.* -import fr.acinq.phoenix.android.R -import fr.acinq.phoenix.android.components.* -import fr.acinq.phoenix.android.components.feedback.ErrorMessage -import fr.acinq.phoenix.android.components.mvi.MVIView -import fr.acinq.phoenix.android.components.settings.Setting -import fr.acinq.phoenix.android.utils.* -import fr.acinq.phoenix.android.utils.extensions.isBadCertificate -import fr.acinq.phoenix.controllers.config.ElectrumConfiguration -import fr.acinq.phoenix.data.ElectrumConfig -import fr.acinq.secp256k1.Hex -import io.ktor.util.network.* -import kotlinx.coroutines.launch -import java.security.MessageDigest -import java.security.cert.X509Certificate -import java.text.DateFormat -import java.text.NumberFormat - -@Composable -fun ElectrumView() { - val nc = navController - val scope = rememberCoroutineScope() - val userPrefs = userPrefs - val electrumServerInPrefs by userPrefs.getElectrumServer.collectAsState(initial = null) - var showCustomServerDialog by rememberSaveable { mutableStateOf(false) } - - DefaultScreenLayout { - DefaultScreenHeader( - onBackClick = { nc.popBackStack() }, - title = stringResource(id = R.string.electrum_title), - ) - Card(internalPadding = PaddingValues(16.dp)) { - Text(text = stringResource(R.string.electrum_about)) - } - MVIView(CF::electrumConfiguration) { model, postIntent -> - Card { - val config = model.configuration - if (showCustomServerDialog) { - ElectrumServerDialog( - initialAddress = electrumServerInPrefs, - onConfirm = { address -> - scope.launch { - userPrefs.saveElectrumServer(address) - postIntent(ElectrumConfiguration.Intent.UpdateElectrumServer(address)) - showCustomServerDialog = false - } - }, - onDismiss = { showCustomServerDialog = false } - ) - } - - // -- connection detail - val connection = model.connection - Setting( - title = when { - connection is Connection.ESTABLISHED -> { - stringResource(id = R.string.electrum_connection_connected, "${model.currentServer?.host}:${model.currentServer?.port}") - } - connection is Connection.ESTABLISHING && config is ElectrumConfig.Random -> { - stringResource(id = R.string.electrum_connection_connecting_to_random, "${model.currentServer?.host}:${model.currentServer?.port}") - } - connection is Connection.ESTABLISHING && config is ElectrumConfig.Custom -> { - stringResource(id = R.string.electrum_connection_connecting_to_custom, config.server.host) - } - connection is Connection.CLOSED && config is ElectrumConfig.Custom -> { - stringResource(id = R.string.electrum_connection_closed_with_custom, config.server.host) - } - else -> { - stringResource(id = R.string.electrum_connection_closed_with_random) - } - }, - subtitle = { - when (config) { - is ElectrumConfig.Custom -> { - if (connection is Connection.CLOSED && connection.isBadCertificate()) { - Text( - text = stringResource(id = R.string.electrum_description_bad_certificate), - style = MaterialTheme.typography.subtitle2.copy(color = negativeColor) - ) - } else { - Text(text = stringResource(id = R.string.electrum_description_custom)) - } - } - else -> Unit - } - }, - leadingIcon = { - PhoenixIcon( - resourceId = R.drawable.ic_server, - tint = when (connection) { - is Connection.ESTABLISHED -> positiveColor - is Connection.ESTABLISHING -> orange - else -> negativeColor - } - ) - }, - maxTitleLines = 1 - ) { showCustomServerDialog = true } - } - - Card { - // block height - if (model.blockHeight > 0) { - val height = remember { NumberFormat.getInstance().format(model.blockHeight) } - Setting(title = stringResource(id = R.string.electrum_block_height_label), description = height) - } - } - } - } -} - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -private fun ElectrumServerDialog( - initialAddress: ServerAddress?, - onConfirm: (ServerAddress?) -> Unit, - onDismiss: () -> Unit -) { - val context = LocalContext.current - val scope = rememberCoroutineScope() - val keyboardManager = LocalSoftwareKeyboardController.current - - var useCustomServer by rememberSaveable { mutableStateOf(initialAddress != null) } - var address by rememberSaveable { mutableStateOf(initialAddress?.run { "$host:$port" } ?: "") } - val host = remember(address) { address.trim().substringBeforeLast(":").takeIf { it.isNotBlank() } } - val port = remember(address) { address.trim().substringAfterLast(":").toIntOrNull() ?: 50002 } - val isOnionHost = remember(address) { host?.endsWith(".onion") ?: false } - - var addressError by rememberSaveable { mutableStateOf(false) } - - val vm = viewModel() - - Dialog( - onDismiss = onDismiss, - buttons = null - ) { - Column(modifier = Modifier.fillMaxWidth()) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp) - ) { - Spacer(Modifier.height(16.dp)) - Checkbox( - text = stringResource(id = R.string.electrum_dialog_checkbox), - checked = useCustomServer, - onCheckedChange = { - addressError = false - if (useCustomServer != it) { - vm.state = ElectrumDialogViewModel.CertificateCheckState.Init - } - useCustomServer = it - }, - enabled = vm.state !is ElectrumDialogViewModel.CertificateCheckState.Checking - ) - TextInput( - modifier = Modifier.fillMaxWidth(), - text = address, - onTextChange = { - addressError = false - if (address != it) { - vm.state = ElectrumDialogViewModel.CertificateCheckState.Init - } - address = it - }, - maxLines = 4, - staticLabel = stringResource(id = R.string.electrum_dialog_input), - enabled = useCustomServer && vm.state !is ElectrumDialogViewModel.CertificateCheckState.Checking, - errorMessage = if (addressError) stringResource(id = R.string.electrum_dialog_invalid_input) else null - ) - if (isOnionHost) { - TextWithIcon( - text = stringResource(id = R.string.electrum_connection_dialog_onion_port), - textStyle = MaterialTheme.typography.subtitle2, - icon = R.drawable.ic_info, - ) - } else { - TextWithIcon( - text = stringResource(id = R.string.electrum_connection_dialog_tls_port), - textStyle = MaterialTheme.typography.subtitle2, - icon = R.drawable.ic_info, - ) - } - } - Spacer(modifier = Modifier.height(12.dp)) - when (val state = vm.state) { - ElectrumDialogViewModel.CertificateCheckState.Init, is ElectrumDialogViewModel.CertificateCheckState.Failure -> { - if (state is ElectrumDialogViewModel.CertificateCheckState.Failure) { - ErrorMessage( - header = stringResource(R.string.electrum_dialog_cert_failure), - details = when (state.e) { - is UnresolvedAddressException -> stringResource(R.string.electrum_dialog_cert_unresolved) - else -> state.e.message ?: state.e.javaClass.simpleName - }, - alignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth() - ) - Spacer(Modifier.height(16.dp)) - } - Row(Modifier.align(Alignment.End)) { - Button(onClick = onDismiss, text = stringResource(id = R.string.btn_cancel)) - Button( - onClick = { - keyboardManager?.hide() - if (useCustomServer) { - val host = host - if (address.matches("""(.*):*(\d*)""".toRegex()) && host != null) { - scope.launch { - if (isOnionHost) { - onConfirm(ServerAddress(host, port, TcpSocket.TLS.DISABLED)) - } else { - vm.checkCertificate(host, port, onCertificateValid = onConfirm) - } - } - } else { - addressError = true - } - } else { - onConfirm(null) - } - }, - enabled = !addressError, - text = if (useCustomServer) { - stringResource(id = R.string.electrum_dialog_cert_check_button) - } else { - stringResource(id = R.string.btn_ok) - }, - ) - } - } - ElectrumDialogViewModel.CertificateCheckState.Checking -> { - Row(Modifier.align(Alignment.End)) { - ProgressView(text = stringResource(id = R.string.electrum_dialog_cert_checking)) - } - } - is ElectrumDialogViewModel.CertificateCheckState.Rejected -> { - val cert = state.certificate - Column( - Modifier - .background(mutedBgColor) - .padding(horizontal = 24.dp, vertical = 12.dp) - ) { - TextWithIcon( - modifier = Modifier.align(Alignment.CenterHorizontally), - icon = R.drawable.ic_alert_triangle, - text = stringResource(R.string.electrum_dialog_cert_header), - textStyle = MaterialTheme.typography.body2 - ) - Spacer(Modifier.height(4.dp)) - Button( - modifier = Modifier.align(Alignment.CenterHorizontally), - text = stringResource(id = R.string.electrum_dialog_cert_copy), - padding = PaddingValues(horizontal = 8.dp, vertical = 4.dp), - space = 8.dp, - textStyle = MaterialTheme.typography.body1.copy(fontSize = 12.sp), - onClick = { copyToClipboard(context, "-----BEGIN CERTIFICATE-----\n${String(Base64.encode(cert.encoded, Base64.DEFAULT), Charsets.US_ASCII)}-----END CERTIFICATE-----") } - ) - Spacer(Modifier.height(12.dp)) - CertDetail( - label = stringResource(id = R.string.electrum_dialog_cert_sha1), - value = Hex.encode(MessageDigest.getInstance("SHA-1").digest(cert.encoded)), - ) - CertDetail( - label = stringResource(id = R.string.electrum_dialog_cert_sha256), - value = Hex.encode(MessageDigest.getInstance("SHA-256").digest(cert.encoded)), - ) - if (cert is X509Certificate) { - CertDetail( - label = stringResource(id = R.string.electrum_dialog_cert_issuer), - value = cert.issuerX500Principal.name.substringAfter("CN=").substringBefore(","), - ) - CertDetail( - label = stringResource(id = R.string.electrum_dialog_cert_subject), - value = cert.issuerX500Principal.name.substringAfter("CN=").substringBefore(","), - ) - CertDetail( - label = stringResource(id = R.string.electrum_dialog_cert_expiration), - value = DateFormat.getDateTimeInstance().format(cert.notAfter), - ) - } - } - Row(Modifier.align(Alignment.End)) { - Button( - text = stringResource(id = R.string.btn_cancel), - space = 8.dp, - onClick = onDismiss - ) - Button( - text = stringResource(id = R.string.electrum_dialog_cert_accept), - icon = R.drawable.ic_check_circle, - iconTint = positiveColor, - space = 8.dp, - onClick = { onConfirm(ServerAddress(state.host, state.port, TcpSocket.TLS.PINNED_PUBLIC_KEY(Base64.encodeToString(cert.publicKey.encoded, Base64.NO_WRAP)))) }, - ) - } - } - } - } - } -} - -@Composable -private fun CertDetail(label: String, value: String) { - Text(text = label, style = MaterialTheme.typography.body2) - Spacer(modifier = Modifier.height(2.dp)) - Text(text = value, style = monoTypo, maxLines = 1) - Spacer(modifier = Modifier.height(4.dp)) -} diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/TorConfigView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/TorConfigView.kt index 5eb36fa8d..cf3aa2bc5 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/TorConfigView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/TorConfigView.kt @@ -16,9 +16,17 @@ package fr.acinq.phoenix.android.settings -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.ButtonDefaults +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -28,120 +36,141 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import fr.acinq.lightning.utils.Connection +import androidx.compose.ui.window.DialogProperties +import fr.acinq.phoenix.android.AppViewModel import fr.acinq.phoenix.android.R +import fr.acinq.phoenix.android.application import fr.acinq.phoenix.android.business -import fr.acinq.phoenix.android.components.* -import fr.acinq.phoenix.android.components.settings.Setting +import fr.acinq.phoenix.android.components.BorderButton +import fr.acinq.phoenix.android.components.Button +import fr.acinq.phoenix.android.components.Card +import fr.acinq.phoenix.android.components.Checkbox +import fr.acinq.phoenix.android.components.DefaultScreenHeader +import fr.acinq.phoenix.android.components.DefaultScreenLayout +import fr.acinq.phoenix.android.components.Dialog +import fr.acinq.phoenix.android.components.ProgressView +import fr.acinq.phoenix.android.components.openLink import fr.acinq.phoenix.android.components.settings.SettingSwitch -import fr.acinq.phoenix.android.navController import fr.acinq.phoenix.android.userPrefs import fr.acinq.phoenix.android.utils.annotatedStringResource -import fr.acinq.phoenix.android.utils.negativeColor -import fr.acinq.phoenix.android.utils.orange -import fr.acinq.phoenix.android.utils.positiveColor +import fr.acinq.phoenix.android.utils.mutedBgColor +import kotlinx.coroutines.delay import kotlinx.coroutines.launch @Composable -fun TorConfigView() { +fun TorConfigView( + appViewModel: AppViewModel, + onBackClick: () -> Unit, + onBusinessTeardown: () -> Unit, +) { val scope = rememberCoroutineScope() val business = business - val nc = navController val userPrefs = userPrefs val torEnabledState = userPrefs.getIsTorEnabled.collectAsState(initial = null) - val connState = business.connectionsManager.connections.collectAsState() var showConfirmTorDialog by remember { mutableStateOf(false) } DefaultScreenLayout { DefaultScreenHeader( - onBackClick = { nc.popBackStack() }, + onBackClick = onBackClick, title = stringResource(id = R.string.tor_settings_title), ) val isTorEnabled = torEnabledState.value Card { if (isTorEnabled == null) { - ProgressView(text = stringResource(id = R.string.tor_settings_unknown), space = 12.dp) + ProgressView(text = stringResource(id = R.string.utils_loading_prefs), space = 12.dp) } else { SettingSwitch( - title = stringResource(id = if (isTorEnabled) R.string.tor_settings_enabled else R.string.tor_settings_disabled), - description = stringResource(id = R.string.tor_settings_subtitle), + title = stringResource(id = R.string.tor_settings_switch_label), icon = R.drawable.ic_tor_shield, enabled = true, isChecked = isTorEnabled, - onCheckChangeAttempt = { enableTor -> - scope.launch { - if (enableTor) { - showConfirmTorDialog = true - } else { - business.appConfigurationManager.updateTorUsage(false) - userPrefs.saveIsTorEnabled(false) - } - } - } + onCheckChangeAttempt = { showConfirmTorDialog = true } ) } } if (isTorEnabled == true) { - Card { - val torState = connState.value.tor - Setting( - title = when (torState) { - is Connection.CLOSED -> stringResource(R.string.tor_settings_state_closed) - Connection.ESTABLISHING -> stringResource(R.string.tor_settings_state_starting) - Connection.ESTABLISHED -> stringResource(R.string.tor_settings_state_started) - else -> stringResource(R.string.tor_settings_state_unknown) - }, - leadingIcon = { - Row(modifier = Modifier.width(width = ButtonDefaults.IconSize), horizontalArrangement = Arrangement.Center) { - Surface( - shape = CircleShape, - color = when (torState) { - Connection.ESTABLISHED -> positiveColor - Connection.ESTABLISHING -> orange - else -> negativeColor - }, - modifier = Modifier.size(8.dp) - ) {} - } - } - ) + Card(internalPadding = PaddingValues(16.dp)) { + val context = LocalContext.current + Text(text = stringResource(id = R.string.tor_settings_instructions_title), style = MaterialTheme.typography.body2) + Spacer(Modifier.height(8.dp)) + Text(text = stringResource(id = R.string.tor_settings_instructions_1)) + Spacer(Modifier.height(8.dp)) + Text(text = stringResource(id = R.string.tor_settings_instructions_2)) + Spacer(Modifier.height(16.dp)) + BorderButton(text = stringResource(id = R.string.tor_settings_instructions_help_button), icon = R.drawable.ic_external_link, onClick = { openLink(context, "https://phoenix.acinq.co/faq#how-to-use-tor-on-phoenix") }) } } } if (showConfirmTorDialog) { var hasReadMessage by remember { mutableStateOf(false) } + val isTorEnabled = torEnabledState.value == true + val application = application Dialog( onDismiss = { showConfirmTorDialog = false }, - title = stringResource(id = R.string.tor_confirm_dialog_title), - buttons = { - Button(text = stringResource(id = R.string.btn_cancel), onClick = { showConfirmTorDialog = false }) - Spacer(modifier = Modifier.width(16.dp)) - Button( - text = stringResource(id = R.string.btn_confirm), - icon = R.drawable.ic_check_circle, - onClick = { - scope.launch { - business.appConfigurationManager.updateTorUsage(true) - userPrefs.saveIsTorEnabled(true) - } - showConfirmTorDialog = false - }, - enabled = hasReadMessage - ) - } + properties = DialogProperties(dismissOnClickOutside = false, dismissOnBackPress = false, usePlatformDefaultWidth = false), + buttons = null ) { - Column(modifier = Modifier.padding(horizontal = 24.dp)) { - Text(text = stringResource(id = R.string.tor_confirm_dialog_details_1)) - Spacer(modifier = Modifier.height(12.dp)) - Text(text = annotatedStringResource(id = R.string.tor_confirm_dialog_details_2)) - Spacer(modifier = Modifier.height(12.dp)) - Checkbox(text = stringResource(id = R.string.utils_ack), checked = hasReadMessage, onCheckedChange = { hasReadMessage = it }) + var isTearingDownBusiness by remember { mutableStateOf(false) } + if (isTearingDownBusiness) { + ProgressView(text = stringResource(id = R.string.tor_dialog_processing), modifier = Modifier.fillMaxWidth(), padding = PaddingValues(32.dp), horizontalArrangement = Arrangement.Center) + } else { + Column(modifier = Modifier.padding(horizontal = 24.dp)) { + Spacer(modifier = Modifier.height(20.dp)) + if (isTorEnabled) { + Text(text = annotatedStringResource(id = R.string.tor_dialog_disable_title), style = MaterialTheme.typography.h4) + Spacer(modifier = Modifier.height(16.dp)) + Text(text = annotatedStringResource(id = R.string.tor_dialog_disable_details_1)) + Spacer(modifier = Modifier.height(12.dp)) + Text(text = annotatedStringResource(id = R.string.tor_dialog_disable_details_2)) + } else { + Text(text = stringResource(id = R.string.tor_dialog_enable_title_1), style = MaterialTheme.typography.h4) + Spacer(modifier = Modifier.height(12.dp)) + Text(text = stringResource(id = R.string.tor_dialog_enable_details_1)) + Spacer(modifier = Modifier.height(16.dp)) + Text(text = annotatedStringResource(id = R.string.tor_dialog_enable_title_2), style = MaterialTheme.typography.h4) + Spacer(modifier = Modifier.height(12.dp)) + Text(text = annotatedStringResource(id = R.string.tor_dialog_enable_details_2)) + Spacer(modifier = Modifier.height(16.dp)) + Surface(color = mutedBgColor, shape = RoundedCornerShape(16.dp)) { + Checkbox(text = stringResource(id = R.string.utils_ack), checked = hasReadMessage, onCheckedChange = { hasReadMessage = it }, padding = PaddingValues(16.dp), modifier = Modifier.fillMaxWidth()) + } + } + } + Spacer(modifier = Modifier.width(32.dp)) + Row( + modifier = Modifier.padding(8.dp).align(Alignment.End) + ) { + Button(text = stringResource(id = R.string.btn_cancel), onClick = { showConfirmTorDialog = false }, shape = RoundedCornerShape(16.dp)) + Spacer(modifier = Modifier.width(16.dp)) + Button( + text = stringResource(id = R.string.btn_confirm), + icon = R.drawable.ic_check_circle, + onClick = { + val service = appViewModel.service ?: return@Button + scope.launch { + isTearingDownBusiness = true + service.shutdown() + application.shutdownBusiness() + business.appConfigurationManager.updateTorUsage(!isTorEnabled) + userPrefs.saveIsTorEnabled(!isTorEnabled) + application.resetBusiness() + delay(500) + showConfirmTorDialog = false + onBusinessTeardown() + } + }, + enabled = isTorEnabled || hasReadMessage, + shape = RoundedCornerShape(16.dp) + ) + } } } } diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/ElectrumDialogViewModel.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/electrum/ElectrumDialogViewModel.kt similarity index 95% rename from phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/ElectrumDialogViewModel.kt rename to phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/electrum/ElectrumDialogViewModel.kt index a367c30dd..a76a9503d 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/ElectrumDialogViewModel.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/electrum/ElectrumDialogViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 ACINQ SAS + * Copyright 2024 ACINQ SAS * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package fr.acinq.phoenix.android.settings +package fr.acinq.phoenix.android.settings.electrum import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -89,8 +89,8 @@ class ElectrumDialogViewModel : ViewModel() { } sealed class CertificateCheckState { - object Init : CertificateCheckState() - object Checking : CertificateCheckState() + data object Init : CertificateCheckState() + data object Checking : CertificateCheckState() data class Rejected(val host: String, val port: Int, val certificate: Certificate) : CertificateCheckState() data class Failure(val e: Throwable) : CertificateCheckState() } diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/electrum/ElectrumView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/electrum/ElectrumView.kt new file mode 100644 index 000000000..2b0c70fb3 --- /dev/null +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/electrum/ElectrumView.kt @@ -0,0 +1,369 @@ +/* + * Copyright 2024 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package fr.acinq.phoenix.android.settings.electrum + + +import android.util.Base64 +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import fr.acinq.lightning.io.TcpSocket +import fr.acinq.lightning.utils.Connection +import fr.acinq.lightning.utils.ServerAddress +import fr.acinq.phoenix.android.* +import fr.acinq.phoenix.android.R +import fr.acinq.phoenix.android.components.* +import fr.acinq.phoenix.android.components.feedback.ErrorMessage +import fr.acinq.phoenix.android.components.mvi.MVIView +import fr.acinq.phoenix.android.components.settings.Setting +import fr.acinq.phoenix.android.utils.* +import fr.acinq.phoenix.android.utils.extensions.isBadCertificate +import fr.acinq.phoenix.controllers.config.ElectrumConfiguration +import fr.acinq.phoenix.data.ElectrumConfig +import fr.acinq.phoenix.utils.extensions.isOnion +import fr.acinq.secp256k1.Hex +import io.ktor.util.network.* +import kotlinx.coroutines.launch +import java.security.MessageDigest +import java.security.cert.X509Certificate +import java.text.DateFormat +import java.text.NumberFormat + +@Composable +fun ElectrumView() { + val nc = navController + val scope = rememberCoroutineScope() + val userPrefs = userPrefs + val electrumServerInPrefs by userPrefs.getElectrumServer.collectAsState(initial = null) + var showCustomServerDialog by rememberSaveable { mutableStateOf(false) } + + DefaultScreenLayout { + DefaultScreenHeader( + onBackClick = { nc.popBackStack() }, + title = stringResource(id = R.string.electrum_title), + ) + Card(internalPadding = PaddingValues(16.dp)) { + Text(text = stringResource(R.string.electrum_about)) + } + MVIView(CF::electrumConfiguration) { model, postIntent -> + Card { + val config = model.configuration + if (showCustomServerDialog) { + ElectrumServerDialog( + initialAddress = electrumServerInPrefs, + onConfirm = { address -> + scope.launch { + userPrefs.saveElectrumServer(address) + postIntent(ElectrumConfiguration.Intent.UpdateElectrumServer(address)) + showCustomServerDialog = false + } + }, + onDismiss = { showCustomServerDialog = false } + ) + } + + // -- connection detail + val connection = model.connection + val torEnabled = userPrefs.getIsTorEnabled.collectAsState(initial = null) + Setting( + title = when { + model.currentServer == null -> { + stringResource(id = R.string.utils_loading_data) + } + connection is Connection.ESTABLISHED -> { + stringResource(id = R.string.electrum_connection_connected, "${model.currentServer?.host}:${model.currentServer?.port}") + } + (connection is Connection.ESTABLISHING || connection is Connection.CLOSED) && config is ElectrumConfig.Random -> { + stringResource(id = R.string.electrum_connection_connecting_to_random, "${model.currentServer?.host}:${model.currentServer?.port}") + } + (connection is Connection.ESTABLISHING || connection is Connection.CLOSED) && config is ElectrumConfig.Custom -> { + stringResource(id = R.string.electrum_connection_connecting_to_custom, config.server.host) + } + else -> { + stringResource(id = R.string.electrum_connection_closed_with_random) + } + }, + subtitle = when (config) { + is ElectrumConfig.Custom -> { + { + if (connection is Connection.CLOSED && connection.isBadCertificate()) { + Text( + text = stringResource(id = R.string.electrum_description_bad_certificate), + style = MaterialTheme.typography.subtitle2.copy(color = negativeColor) + ) + } else if (torEnabled.value == true && !config.server.isOnion) { + Text( + text = stringResource(id = R.string.electrum_description_not_onion), + style = MaterialTheme.typography.subtitle2.copy(color = negativeColor), + ) + } else { + Text(text = stringResource(id = R.string.electrum_description_custom)) + } + } + } + else -> null + }, + leadingIcon = { + PhoenixIcon( + resourceId = R.drawable.ic_server, + tint = when (connection) { + is Connection.ESTABLISHED -> positiveColor + is Connection.ESTABLISHING -> orange + else -> negativeColor + } + ) + }, + maxTitleLines = 1 + ) { showCustomServerDialog = true } + } + + Card { + // block height + if (model.blockHeight > 0) { + val height = remember { NumberFormat.getInstance().format(model.blockHeight) } + Setting(title = stringResource(id = R.string.electrum_block_height_label), description = height) + } + } + } + } +} + +@Composable +private fun ElectrumServerDialog( + initialAddress: ServerAddress?, + onConfirm: (ServerAddress?) -> Unit, + onDismiss: () -> Unit +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + val keyboardManager = LocalSoftwareKeyboardController.current + + var useCustomServer by rememberSaveable { mutableStateOf(initialAddress != null) } + var address by rememberSaveable { mutableStateOf(initialAddress?.run { "$host:$port" } ?: "") } + val host = remember(address) { address.trim().substringBeforeLast(":").takeIf { it.isNotBlank() } } + val port = remember(address) { address.trim().substringAfterLast(":").toIntOrNull() ?: 50002 } + val isOnionHost = remember(address) { host?.endsWith(".onion") ?: false } + val isTorEnabled by userPrefs.getIsTorEnabled.collectAsState(initial = null) + + var addressError by rememberSaveable { mutableStateOf(false) } + val showTorWithoutOnionError = remember(isOnionHost, isTorEnabled, useCustomServer) { useCustomServer && isTorEnabled == true && !isOnionHost } + + val vm = viewModel() + + Dialog( + onDismiss = onDismiss, + buttons = null + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + Spacer(Modifier.height(16.dp)) + Surface(color = mutedBgColor, shape = RoundedCornerShape(16.dp)) { + Checkbox( + text = stringResource(id = R.string.electrum_dialog_checkbox), + checked = useCustomServer, + onCheckedChange = { + addressError = false + if (useCustomServer != it) { + vm.state = ElectrumDialogViewModel.CertificateCheckState.Init + } + useCustomServer = it + }, + enabled = vm.state !is ElectrumDialogViewModel.CertificateCheckState.Checking, + padding = PaddingValues(16.dp), + modifier = Modifier.fillMaxWidth(), + ) + } + Spacer(modifier = Modifier.height(8.dp)) + TextInput( + modifier = Modifier.fillMaxWidth(), + text = address, + onTextChange = { + addressError = false + if (address != it) { + vm.state = ElectrumDialogViewModel.CertificateCheckState.Init + } + address = it + }, + maxLines = 4, + staticLabel = stringResource(id = R.string.electrum_dialog_input), + enabled = useCustomServer && vm.state !is ElectrumDialogViewModel.CertificateCheckState.Checking, + errorMessage = if (addressError) stringResource(id = R.string.electrum_dialog_invalid_input) else if (showTorWithoutOnionError) stringResource(R.string.electrum_connection_dialog_tor_enabled) else null + ) + if (isTorEnabled == true || isOnionHost) { + Spacer(modifier = Modifier.height(4.dp)) + TextWithIcon( + text = stringResource(id = R.string.electrum_connection_dialog_onion_port), + textStyle = MaterialTheme.typography.subtitle2, + icon = R.drawable.ic_info, + ) + } else { + Spacer(modifier = Modifier.height(4.dp)) + TextWithIcon( + text = stringResource(id = R.string.electrum_connection_dialog_tls_port), + textStyle = MaterialTheme.typography.subtitle2, + icon = R.drawable.ic_info, + ) + } + } + Spacer(modifier = Modifier.height(12.dp)) + when (val state = vm.state) { + ElectrumDialogViewModel.CertificateCheckState.Init, is ElectrumDialogViewModel.CertificateCheckState.Failure -> { + if (state is ElectrumDialogViewModel.CertificateCheckState.Failure) { + ErrorMessage( + header = stringResource(R.string.electrum_dialog_cert_failure), + details = when (state.e) { + is UnresolvedAddressException -> stringResource(R.string.electrum_dialog_cert_unresolved) + else -> state.e.message ?: state.e.javaClass.simpleName + }, + alignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) + Spacer(Modifier.height(16.dp)) + } + Row( + Modifier + .align(Alignment.End) + .padding(8.dp)) { + Button(text = stringResource(id = R.string.btn_cancel), onClick = onDismiss, shape = RoundedCornerShape(16.dp)) + Button( + text = if (useCustomServer) { + stringResource(id = R.string.electrum_dialog_cert_check_button) + } else { + stringResource(id = R.string.btn_ok) + }, + onClick = { + keyboardManager?.hide() + if (useCustomServer) { + if (address.matches("""(.*):*(\d*)""".toRegex()) && host != null) { + scope.launch { + if (isOnionHost) { + onConfirm(ServerAddress(host, port, TcpSocket.TLS.DISABLED)) + } else { + vm.checkCertificate(host, port, onCertificateValid = onConfirm) + } + } + } else { + addressError = true + } + } else { + onConfirm(null) + } + }, + enabled = !addressError && !showTorWithoutOnionError, + shape = RoundedCornerShape(16.dp), + ) + } + } + ElectrumDialogViewModel.CertificateCheckState.Checking -> { + Row(Modifier.align(Alignment.End)) { + ProgressView(text = stringResource(id = R.string.electrum_dialog_cert_checking)) + } + } + is ElectrumDialogViewModel.CertificateCheckState.Rejected -> { + val cert = state.certificate + Column( + Modifier + .background(mutedBgColor) + .padding(horizontal = 24.dp, vertical = 12.dp) + ) { + TextWithIcon( + modifier = Modifier.align(Alignment.CenterHorizontally), + icon = R.drawable.ic_alert_triangle, + text = stringResource(R.string.electrum_dialog_cert_header), + textStyle = MaterialTheme.typography.body2 + ) + Spacer(Modifier.height(4.dp)) + Button( + modifier = Modifier.align(Alignment.CenterHorizontally), + text = stringResource(id = R.string.electrum_dialog_cert_copy), + padding = PaddingValues(horizontal = 8.dp, vertical = 4.dp), + space = 8.dp, + textStyle = MaterialTheme.typography.body1.copy(fontSize = 12.sp), + onClick = { copyToClipboard(context, "-----BEGIN CERTIFICATE-----\n${String(Base64.encode(cert.encoded, Base64.DEFAULT), Charsets.US_ASCII)}-----END CERTIFICATE-----") } + ) + Spacer(Modifier.height(12.dp)) + CertDetail( + label = stringResource(id = R.string.electrum_dialog_cert_sha1), + value = Hex.encode(MessageDigest.getInstance("SHA-1").digest(cert.encoded)), + ) + CertDetail( + label = stringResource(id = R.string.electrum_dialog_cert_sha256), + value = Hex.encode(MessageDigest.getInstance("SHA-256").digest(cert.encoded)), + ) + if (cert is X509Certificate) { + CertDetail( + label = stringResource(id = R.string.electrum_dialog_cert_issuer), + value = cert.issuerX500Principal.name.substringAfter("CN=").substringBefore(","), + ) + CertDetail( + label = stringResource(id = R.string.electrum_dialog_cert_subject), + value = cert.issuerX500Principal.name.substringAfter("CN=").substringBefore(","), + ) + CertDetail( + label = stringResource(id = R.string.electrum_dialog_cert_expiration), + value = DateFormat.getDateTimeInstance().format(cert.notAfter), + ) + } + } + Row( + Modifier + .align(Alignment.End) + .padding(8.dp)) { + Button( + text = stringResource(id = R.string.btn_cancel), + onClick = onDismiss, + space = 8.dp, + shape = RoundedCornerShape(16.dp), + ) + Button( + text = stringResource(id = R.string.electrum_dialog_cert_accept), + onClick = { onConfirm(ServerAddress(state.host, state.port, TcpSocket.TLS.PINNED_PUBLIC_KEY(Base64.encodeToString(cert.publicKey.encoded, Base64.NO_WRAP)))) }, + icon = R.drawable.ic_check_circle, + iconTint = positiveColor, + space = 8.dp, + shape = RoundedCornerShape(16.dp), + ) + } + } + } + } +} + +@Composable +private fun CertDetail(label: String, value: String) { + Text(text = label, style = MaterialTheme.typography.body2) + Spacer(modifier = Modifier.height(2.dp)) + Text(text = value, style = monoTypo, maxLines = 1) + Spacer(modifier = Modifier.height(4.dp)) +} diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/Theme.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/Theme.kt index c8cc2bcd0..890294cbf 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/Theme.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/Theme.kt @@ -80,7 +80,7 @@ val gray200 = Color(0xFFB5BBC9) val gray100 = Color(0xffd1d7e3) val gray70 = Color(0xFFDDE8EB) val gray50 = Color(0xFFE9F1F3) -val gray30 = Color(0xFFEFF4F5) +val gray30 = Color(0xFFF2F6F7) val gray20 = Color(0xFFF4F7F9) val gray10 = Color(0xFFF9FAFC) diff --git a/phoenix-android/src/main/res/values-b+es+419/important_strings.xml b/phoenix-android/src/main/res/values-b+es+419/important_strings.xml index 7aa01ccfb..dd2c496a3 100644 --- a/phoenix-android/src/main/res/values-b+es+419/important_strings.xml +++ b/phoenix-android/src/main/res/values-b+es+419/important_strings.xml @@ -92,7 +92,6 @@ Procesamiento en segundo plano restringido. Phoenix no podrá recibir pagos cuando esté en segundo plano, o cuando esté cerrado. Esto ocurre porque: - Tor está activado El dispositivo está en modo de ahorro de energía FCM notificaciones no disponibles Si utilizas GrapheneOS o CalyxOS, instala Google Play Services para recibir notificaciones de FCM. Consulta las preguntas frecuentes para obtener más información. @@ -280,12 +279,6 @@ - Phoenix puede usar Tor para proteger tu IP de los demás nodos de la red Lightning y los servidores de Electrum. - - ¿Habilitar Tor? - Tor es lento y afectará al rendimiento de tus pagos. - Específicamente, no podrá recibir pagos si Phoenix está en segundo plano, o si está cerrado. - Phoenix es una billetera de Bitcoin que usa la red Lightning para enviar y recibir pagos.\n\nEs un software libre y de código abierto, desarrollado por ACINQ conforme a la licencia Apache 2.0. diff --git a/phoenix-android/src/main/res/values-b+es+419/strings.xml b/phoenix-android/src/main/res/values-b+es+419/strings.xml index 95a1a7857..d4af09aac 100644 --- a/phoenix-android/src/main/res/values-b+es+419/strings.xml +++ b/phoenix-android/src/main/res/values-b+es+419/strings.xml @@ -257,16 +257,16 @@ - Cargando… - +%1$s + Cargando… + +%1$s - FAQ - Usa los botones Recibir y Enviar en la parte inferior de esta pantalla para empezar. - Mostrar todos los pagos… - Retraso de Electrum - Certificado de Electrum - Conectando… - Tor + FAQ + Usa los botones Recibir y Enviar en la parte inferior de esta pantalla para empezar. + Mostrar todos los pagos… + Retraso de Electrum + Certificado de Electrum + Conectando… + Tor @@ -542,27 +542,17 @@ Tor - Comprobando preferencias… - Tor está activada - Tor está desactivada - El proxy de Tor aún no se ha iniciado. - El proxy de Tor está apagado. \nEspera y comprueba tu conexión a Internet. - Iniciando proxy de Tor… - Tor está en línea Estado de las conexiones La aplicación no funcionará correctamente hasta que se hayan establecido todas las conexiones. Tu dispositivo no tiene conexión a Internet. La aplicación no funcionará correctamente.\n\nComprueba la configuración de tu dispositivo. - Internet Electrum - Tor Par Gestionar la conexión para %1$s Conectando… Conectado - El servidor de Electrum registra un retraso (bloque $1$d) Desconectado Certificado defectuoso diff --git a/phoenix-android/src/main/res/values-cs/important_strings.xml b/phoenix-android/src/main/res/values-cs/important_strings.xml index d081792b1..e97888b49 100644 --- a/phoenix-android/src/main/res/values-cs/important_strings.xml +++ b/phoenix-android/src/main/res/values-cs/important_strings.xml @@ -95,7 +95,6 @@ Omezeno zpracovávání na pozadí Phoenix nebude moci přijímat platby, pokud je na pozadí nebo pokud je zavřený.. Děje se tak, protože: - Tor je povolen Zařízení je v úsporném režimu Oznámení FCM nejsou k dispozici Pokud používáte GrapheneOS nebo CalyxOS, nainstalujte si služby Google Play, abyste mohli dostávat FCM oznámení. Nápovědu naleznete v často kladených dotazech. @@ -282,12 +281,6 @@ - Phoenix může použít Tor ke skrytí vaší IP adresy před ostatními uzly v síti Lightning a servery Electrum. - - Povolit Tor? - Tor je pomalý a ovlivní rychlost vašich plateb. - Konkrétně nebudete moci přijímat platby, pokud je Phoenix na pozadí nebo pokud je zavřený. - Phoenix je Bitcoinová peněženka využívající síť Lightning pro odesílání a přijímání plateb.\n\nJedná se o bezplatný software s otevřeným zdrojovým kódem, který vyvinula společnost ACINQ pod licencí Apache 2.0. diff --git a/phoenix-android/src/main/res/values-cs/strings.xml b/phoenix-android/src/main/res/values-cs/strings.xml index 152576630..a767af6a9 100644 --- a/phoenix-android/src/main/res/values-cs/strings.xml +++ b/phoenix-android/src/main/res/values-cs/strings.xml @@ -237,16 +237,16 @@ - Načítání… - +%1$s + Načítání… + +%1$s - FAQ - Pomocí tlačítek Přijmout a Odeslat v dolní části této obrazovky můžete začít! - Ukázat všechny platby… - Electrum late - Electrum certifikát - Připojování… - Tor + FAQ + Pomocí tlačítek Přijmout a Odeslat v dolní části této obrazovky můžete začít! + Ukázat všechny platby… + Electrum late + Electrum certifikát + Připojování… + Tor @@ -549,27 +549,17 @@ Tor - Kontrola nastavneí… - Tor je zapnutý - Tor je vypnutý - Tor proxy server ještě nebyl spuštěn. - Tor proxy server je vypnutý. - Zapínání Tor proxy serveru… - Tor je připojen Stav připojení Aplikace nebude správně fungovat, dokud nebudou navázána všechna připojení. Vaše zařízení nemá připojení k internetu. Aplikace nebude fungovat správně.\n\nZkontrolujte prosím nastavení vašeho zařízení. - Internet Electrum - Tor Peer Spravovat připojení pro %1$s Připojování… Připojeno - Electrum server zaostává (blok $1$d) Odpojeno Špatný certifikát diff --git a/phoenix-android/src/main/res/values-de/important_strings.xml b/phoenix-android/src/main/res/values-de/important_strings.xml index c7ea67454..824cf70f8 100644 --- a/phoenix-android/src/main/res/values-de/important_strings.xml +++ b/phoenix-android/src/main/res/values-de/important_strings.xml @@ -92,7 +92,6 @@ Hintergrundverarbeitung eingeschränkt Phoenix kann keine Zahlungen empfangen, wenn es im Hintergrund läuft oder geschlossen ist. Dies geschieht, weil: - Tor ist aktiviert Das Gerät ist im Energiesparmodus FCM-Benachrichtigungen nicht verfügbar Wenn Sie GrapheneOS oder CalyxOS verwenden, installieren Sie Google Play Services, um FCM-Benachrichtigungen zu erhalten. Lesen Sie die FAQ für Hilfe. @@ -279,12 +278,6 @@ - Phoenix kann Tor verwenden, um Ihre IP vor den anderen Knoten im Lightning Netzwerk und den Electrum Servern zu schützen. - - Tor aktivieren? - Tor ist langsam und wird die Leistung Ihrer Zahlungen beeinträchtigen. - Speziell werden Sie keine Zahlungen empfangen können, wenn Phoenix im Hintergrund läuft oder geschlossen ist. - Phoenix ist eine Bitcoin-Wallet, die das Lightning-Netzwerk zum Senden und Empfangen von Zahlungen nutzt.\n\nEs ist eine kostenlose Open-Source-Software, entwickelt von ACINQ unter der Apache 2.0 Lizenz. diff --git a/phoenix-android/src/main/res/values-de/strings.xml b/phoenix-android/src/main/res/values-de/strings.xml index ef064031c..aa3a7038d 100644 --- a/phoenix-android/src/main/res/values-de/strings.xml +++ b/phoenix-android/src/main/res/values-de/strings.xml @@ -261,16 +261,16 @@ - Lädt… - +%1$s - - FAQ - Verwenden Sie die Schaltflächen Empfangen und Senden unten auf diesem Fenster, um loszulegen! - Alle Zahlungen anzeigen… - Electrum unsynchron - Electrum-Zertifikat - Verbinde… - Tor + Lädt… + +%1$s + + FAQ + Verwenden Sie die Schaltflächen Empfangen und Senden unten auf diesem Fenster, um loszulegen! + Alle Zahlungen anzeigen… + Electrum unsynchron + Electrum-Zertifikat + Verbinde… + Tor Liquidität anfordern Du hast derzeit %1$d Zahlung(en) in deiner Brieftasche ausstehend.\n\nHalte die App geöffnet, um sicherzustellen, dass diese Zahlungen ohne Probleme abgerechnet werden. @@ -554,27 +554,17 @@ Tor - Einstellungen werden geprüft… - Tor ist aktiviert - Tor ist deaktiviert - Der Tor-Proxy ist noch nicht gestartet. - Der Tor-Proxy wird abgeschaltet.\nBitte warten Sie und überprüfen Sie Ihre Internetverbindung. - Starte Tor-Proxy… - Tor ist online Verbindungs-Status Die App funktioniert erst dann richtig, wenn alle Verbindungen hergestellt sind. Ihr Gerät hat keine Internetverbindung. Die App wird nicht richtig funktionieren.\n\nBitte überprüfen Sie die Einstellungen Ihres Geräts. - Internet Electrum - Tor Gegenstelle Verbindung verwalten für %1$s Verbinde… Verbunden - Electrum-Server ist nicht synchronisiert (Block $1$d) Getrennt Ungültiges Zertifikat diff --git a/phoenix-android/src/main/res/values-es/important_strings.xml b/phoenix-android/src/main/res/values-es/important_strings.xml index 50b3a6214..ea0bdac44 100644 --- a/phoenix-android/src/main/res/values-es/important_strings.xml +++ b/phoenix-android/src/main/res/values-es/important_strings.xml @@ -90,7 +90,6 @@ Procesamiento en segundo plano restringido. Phoenix no podrá recibir pagos cuando esté en segundo plano, o cuando esté cerrado. Esto ocurre porque: - Tor está activado El dispositivo está en modo de ahorro de energía FCM notificaciones no disponibles Si utilizas GrapheneOS o CalyxOS, instala Google Play Services para recibir notificaciones de FCM. Consulta las preguntas frecuentes para obtener más información. @@ -283,12 +282,6 @@ - Phoenix puede utilizar Tor para proteger su IP de los otros nodos de la Red Lightning y de los servidores de Electrum. - - ¿Habilitar Tor? - Tor es lento y afectará al rendimiento de tus pagos. - Específicamente, no podrá recibir pagos si Phoenix está en segundo plano, o si está cerrado. - Phoenix es una cartera Bitcoin que utiliza la red Lightning para enviar y recibir pagos.\n\nEs un programa libre de código abierto, desarrollado por ACINQ en virtud de la licencia Apache 2.0. diff --git a/phoenix-android/src/main/res/values-fr/important_strings.xml b/phoenix-android/src/main/res/values-fr/important_strings.xml index 89ea61f97..5c24ecf4f 100644 --- a/phoenix-android/src/main/res/values-fr/important_strings.xml +++ b/phoenix-android/src/main/res/values-fr/important_strings.xml @@ -95,7 +95,6 @@ Fonctionnement en arrière plan restreint Phoenix ne pourra pas recevoir de paiements quand l\'application est en arrière plan ou est fermée. Cela vient du fait que: - Tor est activé Le téléphone est en mode économie d\'énergie Les notifications FCM sont indisponibles Si vous êtes sur GrapheneOS ou CalyxOS, installez les Google Play Services pour avoir accès aux notifications FCM. Consultez la FAQ pour plus d\'infos. @@ -283,12 +282,6 @@ - Phoenix peut utiliser Tor pour masquer votre IP des autres noeuds du réseau Lightning et des serveurs Electrum. - - Activer Tor ? - Tor est lent et va impacter les performances de vos paiements. - En particulier, vous ne pourrez pas recevoir de paiements si Phoenix est en arrière-plan, ou est fermée. - Phoenix est un wallet Bitcoin qui utilise le réseau Lightning pour envoyer et recevoir des paiements.\n\nC\'est un logiciel libre et en source ouverte, développé par ACINQ sous licence Apache 2.0. diff --git a/phoenix-android/src/main/res/values-fr/strings.xml b/phoenix-android/src/main/res/values-fr/strings.xml index 74ae66701..17af92935 100644 --- a/phoenix-android/src/main/res/values-fr/strings.xml +++ b/phoenix-android/src/main/res/values-fr/strings.xml @@ -268,16 +268,16 @@ - Chargement… - +%1$s + Chargement… + +%1$s - FAQ - Utilisez les boutons Envoyer et Recevoir ci-dessous pour débuter ! - Montrer tous les paiements… - Electrum en retard - Certificat Electrum - Connexion… - Tor + FAQ + Utilisez les boutons Envoyer et Recevoir ci-dessous pour débuter ! + Montrer tous les paiements… + Electrum en retard + Certificat Electrum + Connexion… + Tor @@ -594,26 +594,16 @@ Tor - Vérification des préférences… - Tor est activé - Tor est désactivé - Le proxy Tor n\'est pas encore démarré. - Le proxy Tor est éteint. - Démarrage du proxy Tor… - Tor est en fonctionnement Statut des connexions L\'application ne fonctionnera pas correctement tant que chaque connexion n\'est pas établie. Votre appareil n\'a pas de connexion internet. L\'application ne fonctionnera pas correctement.\n\nVérifiez votre configuration réseau. - Internet Electrum - Tor Lightning Connexion… Connecté - Serveur électrum en retard (bloc $1$d) Déconnecté Certificat invalide diff --git a/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml b/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml index 671bb1ee0..08ae0aa45 100644 --- a/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml +++ b/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml @@ -95,7 +95,6 @@ Processamento em segundo plano restrito O Phoenix não poderá receber pagamentos quando estiver em segundo plano ou quando estiver fechado. Isso acontece porque: - Tor está ativado O dispositivo está no modo de economia de energia FCM notificações indisponíveis Se você estiver usando GrapheneOS ou CalyxOS, instale o Google Play Services para receber notificações FCM. Consulte as Perguntas frequentes para obter ajuda. @@ -282,8 +281,6 @@ - A Phoenix pode usar o Tor para proteger seu IP dos outros nós da Lightning Network e dos servidores da Electrum. - A Phoenix é uma carteira de Bitcoin que usa a rede Lightning para enviar e receber pagamentos.\n\nÉ um software de código aberto livre, desenvolvido por ACINQ sob a licença do Apache 2.0. diff --git a/phoenix-android/src/main/res/values-pt-rBR/strings.xml b/phoenix-android/src/main/res/values-pt-rBR/strings.xml index 8d811f980..caf5ae35a 100644 --- a/phoenix-android/src/main/res/values-pt-rBR/strings.xml +++ b/phoenix-android/src/main/res/values-pt-rBR/strings.xml @@ -254,16 +254,16 @@ - Carregando… - +%1$s + Carregando… + +%1$s - Perguntas frequentes - Use os botões Receber e Enviar na parte inferior desta tela para começar. - Mostrar todos os pagamentos… - Atraso de Eletro - Certificado Electro - Conectando… - Tor + Perguntas frequentes + Use os botões Receber e Enviar na parte inferior desta tela para começar. + Mostrar todos os pagamentos… + Atraso de Eletro + Certificado Electro + Conectando… + Tor @@ -539,27 +539,17 @@ Tor - Verificando preferências… - Tor está ativado - Tor está desabilitado - O proxy Tor ainda não foi iniciado. - O proxy Tor está desativado.\nAguarde e verifique sua conexão com a Internet. - Iniciando o proxy Tor… - Tor está online Status da conexão O aplicativo não funcionará corretamente até que todas as conexões tenham sido estabelecidas. Seu dispositivo não possui conexão com a Internet. O app não funcionará corretamente.\n\nVerifique as configurações do seu dispositivo. - Internet Electrum - Tor Par Gerenciar conexão para %1$s Conectando… Conectado - O servidor Electrum registra um atraso (bloco $1$d) Desconectado Certificado com defeito diff --git a/phoenix-android/src/main/res/values-sk/important_strings.xml b/phoenix-android/src/main/res/values-sk/important_strings.xml index 0ad31fb4f..43672abb7 100644 --- a/phoenix-android/src/main/res/values-sk/important_strings.xml +++ b/phoenix-android/src/main/res/values-sk/important_strings.xml @@ -95,7 +95,6 @@ Omedzené spracovanie pozadia Fénix nebude môcť prijímať platby, keď je na pozadí alebo keď je zatvorený. Deje sa tak, pretože: - Tor je povolený Zariadenie je v úspornom režime Oznamy FCM nie sú k dispozícii Ak používate GrapheneOS alebo CalyxOS, nainštalujte si Google Play Services, aby ste mohli dostávať oznámenia FCM. Pozrite si často kladené otázky, kde nájdete návod. @@ -282,12 +281,6 @@ - Phoenix môže použiť Tor na skrytie vašej IP adresy pred ostatnými uzlami siete Lightning Network a servermi Electrum. - - Povolenie Tor? - Tor je pomalý a ovplyvní výkon vašich platieb. - Konkrétne, nebudete môcť prijímať platby, ak je Phoenix na pozadí alebo ak je zatvorený. - Phoenix je Bitcoin peňaženka využívajúca sieť Lightning na odosielanie a prijímanie platieb.\n\nJe to bezplatný softvér s otvoreným zdrojovým kódom, ktorý vyvinula spoločnosť ACINQ pod licenciou Apache 2.0. diff --git a/phoenix-android/src/main/res/values-sk/strings.xml b/phoenix-android/src/main/res/values-sk/strings.xml index 3a0ad4415..c32e1abfd 100644 --- a/phoenix-android/src/main/res/values-sk/strings.xml +++ b/phoenix-android/src/main/res/values-sk/strings.xml @@ -274,16 +274,16 @@ - Načítavanie… - +%1$s - - FAQ - Pomocou tlačidiel Prijať a Odoslať v dolnej časti tejto obrazovky môžete začať! - Zobraziť všetky platby… - Electrum mešká - Electrum certifikát - Pripájanie… - Tor + Načítavanie… + +%1$s + + FAQ + Pomocou tlačidiel Prijať a Odoslať v dolnej časti tejto obrazovky môžete začať! + Zobraziť všetky platby… + Electrum mešká + Electrum certifikát + Pripájanie… + Tor Vyžiadať likviditu Momentálne máte %1$d platieb nepotvrdených vo vašej peňaženke.\n\nNechajte aplikáciu otvorenú, aby ste sa uistili, že sa tieto platby správne a bez problémov vyrovnajú.. @@ -595,27 +595,17 @@ Tor - Kontrola nastavení… - Tor je zapnutý - Tor je vypnutý - Tor proxy server ešte nebol spustený. - Tor proxy server je vypnutý.\nPočkajte prosím a skontrolujte svoje internetové pripojenie. - Zapínanie Tor proxy servera… - Tor je pripojený Stav pripojenia Aplikácia nebude správne fungovať, kým nebudú nadviazané všetky pripojenia. Vaše zariadenie nemá pripojenie k internetu. Aplikácia nebude fungovať správne.\n\nSkontrolujte prosím nastavenia vášho zariadenia. - Internet Electrum - Tor Peer Spravovať pripojenia pre %1$s Pripájanie… Pripojené - Electrum server zaostáva (blok $1$d) Odpojené Zlý certifikát diff --git a/phoenix-android/src/main/res/values-sw/important_strings.xml b/phoenix-android/src/main/res/values-sw/important_strings.xml index 2f9ed043f..b1163d345 100644 --- a/phoenix-android/src/main/res/values-sw/important_strings.xml +++ b/phoenix-android/src/main/res/values-sw/important_strings.xml @@ -99,7 +99,6 @@ Usindikaji wa nyuma umepunguzwa Phoenix haitaweza kupokea malipo wakati iko nyuma, au wakati imefungwa. Hii hutokea kwa sababu: - Tor imewashwa Kifaa kipo kwenye hali ya kuokoa nguvu Arifa za FCM hazipatikani Kama uko kwenye GrapheneOS au CalyxOS, weka Huduma za Google Play kupata arifa za FCM. Angalia FAQ kwa mwongozo. @@ -285,12 +284,6 @@ - Phoenix inaweza kutumia Tor kuficha IP yako kutoka kwa nodi zingine kwenye Mtandao wa Lightning na seva za Electrum. - - Wezesha Tor? - Tor ni polepole na itaathiri utendaji wa malipo yako. - Hasa, hutoweza kupokea malipo ikiwa Phoenix iko kwenye usuli, au ikiwa imefungwa. - Phoenix ni pochi ya Bitcoin inayotumia mtandao wa Lightning kwa kutuma na kupokea malipo.\n\nNi programu huru ya chanzo wazi, iliyotengenezwa na ACINQ chini ya Leseni ya Apache 2.0. diff --git a/phoenix-android/src/main/res/values-sw/strings.xml b/phoenix-android/src/main/res/values-sw/strings.xml index 37e109853..443f7f10e 100644 --- a/phoenix-android/src/main/res/values-sw/strings.xml +++ b/phoenix-android/src/main/res/values-sw/strings.xml @@ -293,16 +293,16 @@ - Inapakia… - +%1$s - - Maswali Yanayoulizwa Mara Kwa Mara (FAQ) - Tumia vitufe vya Kupokea na Kutuma vilivyo chini ya skrini hii ili kuanza! - Onyesha malipo yote… - Electrum imechelewa - Cheti cha Electrum - Inajiunganisha… - Tor + Inapakia… + +%1$s + + Maswali Yanayoulizwa Mara Kwa Mara (FAQ) + Tumia vitufe vya Kupokea na Kutuma vilivyo chini ya skrini hii ili kuanza! + Onyesha malipo yote… + Electrum imechelewa + Cheti cha Electrum + Inajiunganisha… + Tor Omba fedha Kwa sasa una malipo %1$d yanayosubiri kwenye pochi yako.\n\nHifadhi programu wazi ili kuhakikisha malipo haya yanaweza kutekelezwa vizuri bila matatizo. @@ -617,27 +617,17 @@ Tor - Inakagua mapendeleo… - Tor imewezeshwa - Tor haijawezeshwa - Kipande cha Tor hakijaanza bado. - Kipande cha Tor kimefungwa. - Kuanza kwa kipande cha Tor… - Tor iko mtandaoni Hali ya Muunganisho Programu haitafanya kazi ipasavyo hadi muunganisho wote utakapowekwa. Kifaa chako hakina muunganisho wa Intaneti. Programu haitafanya kazi vizuri.\n\nTafadhali angalia mipangilio ya kifaa chako. - Intaneti Electrum - Tor Mwenzi Simamia muunganisho kwa %1$s Kuunganisha… Imepangwa - Seva ya Electrum inachelewa (block $1$d) Imetenganishwa Cheti kibaya diff --git a/phoenix-android/src/main/res/values-vi/important_strings.xml b/phoenix-android/src/main/res/values-vi/important_strings.xml index b2f780f20..e38a775d3 100644 --- a/phoenix-android/src/main/res/values-vi/important_strings.xml +++ b/phoenix-android/src/main/res/values-vi/important_strings.xml @@ -102,7 +102,6 @@ Hạn chế xử lý nền Phoenix sẽ không thể nhận thanh toán khi nó ở chế độ nền hoặc khi nó bị đóng. Điều này xảy ra vì: - Tor đã được bật Thiết bị đang ở chế độ tiết kiệm năng lượng Thông báo FCM không khả dụng Nếu bạn đang sử dụng GrapheneOS hoặc CalyxOS, hãy cài đặt Dịch vụ Google Play để nhận thông báo FCM. Hãy xem Câu hỏi thường gặp để được trợ giúp. @@ -289,12 +288,6 @@ - Phoenix có thể sử dụng Tor để bảo vệ IP của bạn khỏi các nút mạng khác trên mạng Lightning và máy chủ Electrum. - - Bật Tor ? - Tor chạy chậm và sẽ ảnh hưởng đến hiệu suất thanh toán của bạn. - Đặc biệt, bạn sẽ không thể nhận thanh toán nếu Phoenix ở chế độ nền hoặc nếu nó bị đóng. - Phoenix là ví tiền điện tử Bitcoin sử dụng mạng Lightning để gửi và nhận các thanh toán.\n\nĐây là một phần mềm có mã nguồn mở và miễn phí, được lập trình bởi ACINQ theo bản quyền Apache 2.0. diff --git a/phoenix-android/src/main/res/values-vi/strings.xml b/phoenix-android/src/main/res/values-vi/strings.xml index ec9f2cec5..7166ba481 100644 --- a/phoenix-android/src/main/res/values-vi/strings.xml +++ b/phoenix-android/src/main/res/values-vi/strings.xml @@ -263,16 +263,16 @@ - Đang tải… - +%1$s - - FAQ - Sử dụng các nút Nhận và Gửi ở cuối màn hình này để bắt đầu! - Hiển thị tất cả các khoản thanh toán… - Electrum muộn - Chứng nhận Electrum - Đang kết nối… - Tor + Đang tải… + +%1$s + + FAQ + Sử dụng các nút Nhận và Gửi ở cuối màn hình này để bắt đầu! + Hiển thị tất cả các khoản thanh toán… + Electrum muộn + Chứng nhận Electrum + Đang kết nối… + Tor Yêu cầu thanh khoản @@ -554,27 +554,17 @@ Tor - Đang kiểm tra tùy chọn… - Đã bật Tor - Đã tắt Tor - Proxy của Tor chưa được khởi động. - Proxy của Tor đang bị đóng.\nVui lòng chờ đợi và kiểm tra kết nối mạng Internet của bạn. - Đang khởi động Proxy của Tor… - Tor đang kết nối trực tuyến Trạng thái kết nối Ứng dụng sẽ không hoạt động chuẩn xác cho đến khi tất cả các kết nối được thiết lập. Thiết bị của bạn không có kết nối Internet. Ứng dụng sẽ không hoạt động hiệu quả.\n\nVui lòng kiểm tra cài đặt thiết bị của bạn. - Internet Electrum - Tor Máy ngang hàng Quản lý kết nối cho %1$s Đang kết nối… Đã kết nối - Máy chủ Electrum bị chậm (khối $1$d) Đã ngắt kết nối Chứng nhận tồi diff --git a/phoenix-android/src/main/res/values/important_strings.xml b/phoenix-android/src/main/res/values/important_strings.xml index 07c7ef7f6..864343ebf 100644 --- a/phoenix-android/src/main/res/values/important_strings.xml +++ b/phoenix-android/src/main/res/values/important_strings.xml @@ -97,9 +97,8 @@ These funds come from closed Lightning channels. They must be spent manually. Background processing restricted - Phoenix will not be able to receive payments when it is in the background, or when it is closed. + Phoenix may not be able to receive payments when it is in the background, or when it is closed. This happens because: - Tor is enabled The device is in power saving mode FCM notifications unavailable If you\'re on GrapheneOS or CalyxOS, install Google Play Services to get FCM notifications. Check the FAQ for guidance. @@ -173,8 +172,7 @@ Tor is enabled - Phoenix cannot receive payments in the background. - To avoid payment failures, use Orbot instead, or make sure Phoenix remain visible in the foreground when payments are sent to you. + Phoenix may have issues receiving payments. Make sure the app stays open in the foreground and that connection is stable. @@ -286,11 +284,14 @@ - Phoenix can use Tor to shield your IP from the other nodes in the Lightning Network and Electrum servers. - - Enable Tor ? - Tor is slow and will impact the performance of your payments. - Specifically, you will not be able to receive payments if Phoenix is in the background, or if it\'s closed. + Using onion services + Enabling this option will force Phoenix to only connect to onion services for Lightning and Electrum. Note that a Tor proxy application is required. + Be advised + Tor can improve privacy, but may cause performance issues and missed payments. + Disabling onion services + If you disable this option, connections will happen on clearnet. + Are you sure you want to proceed ? + Processing changes… diff --git a/phoenix-android/src/main/res/values/strings.xml b/phoenix-android/src/main/res/values/strings.xml index 49352f45c..743e43122 100644 --- a/phoenix-android/src/main/res/values/strings.xml +++ b/phoenix-android/src/main/res/values/strings.xml @@ -338,16 +338,17 @@ - Loading… - +%1$s - - FAQ - Use the Receive and Send buttons at the bottom of this screen to get started! - Show all payments… - Electrum late - Electrum certificate - Connecting… - Tor + Loading… + +%1$s + + FAQ + Use the Receive and Send buttons at the bottom of this screen to get started! + Show all payments… + Desync! + Certificate + Invalid address + Connecting… + Tor Request liquidity You currently have %1$d payment(s) pending in your wallet.\n\nKeep the app open to make sure these payments settle properly without issues. @@ -652,7 +653,8 @@ To secure your payment channels Phoenix monitors the Bitcoin blockchain through Electrum servers.\n\nBy default, random servers are used. You can also configure Phoenix to connect only to your own server. Block height Use the TLS port (default 50002). - Use the plain TCP port, not the TLS one, since this is an onion service. + For onion services, use the plain TCP port, not the TLS one. + Must be an onion address Disconnected from Electrum Disconnected from %1$s @@ -662,6 +664,7 @@ You are using a custom server This server provided an unknown certificate. Connection is rejected. + Tor is enabled. This server must use an onion address. Use a custom server Server address (host:port) @@ -682,29 +685,27 @@ Tor - Checking preferences… - Tor is enabled - Tor is disabled - The Tor proxy is not started yet. - The Tor proxy is shutdown. - Starting Tor proxy… - Tor is online + Use onion services + How it works + With Tor enabled, Phoenix will only use onion addresses to connect to Lightning and Electrum. + A Tor VPN must also be running on your device. This VPN is not provided by Phoenix. You need to install one. + Learn more Connections status - The app will not function correctly until all connections are established. + Some connections are not established yet. The app will not function correctly until they are. Your device has no Internet connection. The app will not function properly.\n\nPlease check your device\'s setting. - Internet Electrum - Tor Peer Manage connection for %1$s Connecting… Connected - Electrum server is late (block $1$d) Disconnected - Bad certificate + Bad certificate! + Invalid address! + Tor is enabled! + Make sure your Tor VPN is active and running. diff --git a/phoenix-shared/build.gradle.kts b/phoenix-shared/build.gradle.kts index 8f57b666a..fb6ca6c94 100644 --- a/phoenix-shared/build.gradle.kts +++ b/phoenix-shared/build.gradle.kts @@ -59,7 +59,7 @@ kotlin { } } - listOf(iosX64(), iosArm64()).forEach { + listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach { it.binaries { framework { optimized = false @@ -89,7 +89,6 @@ kotlin { dependencies { // lightning-kmp api("fr.acinq.lightning:lightning-kmp:${Versions.lightningKmp}") - api("fr.acinq.tor:tor-mobile-kmp:${Versions.torMobile}") // ktor implementation("io.ktor:ktor-client-core:${Versions.ktor}") implementation("io.ktor:ktor-client-json:${Versions.ktor}") diff --git a/phoenix-shared/src/androidMain/kotlin/fr/acinq/phoenix/managers/AppConnectionsDaemon.kt b/phoenix-shared/src/androidMain/kotlin/fr/acinq/phoenix/managers/AppConnectionsDaemon.kt deleted file mode 100644 index 7aa0a622d..000000000 --- a/phoenix-shared/src/androidMain/kotlin/fr/acinq/phoenix/managers/AppConnectionsDaemon.kt +++ /dev/null @@ -1,16 +0,0 @@ -package fr.acinq.phoenix.managers - -import fr.acinq.tor.Tor -import kotlinx.coroutines.* - -@OptIn(ExperimentalStdlibApi::class) -actual suspend fun Tor.startInProperScope(scope: CoroutineScope) { - val currentDispatcher = scope.coroutineContext[CoroutineDispatcher.Key] - if (currentDispatcher != Dispatchers.Default || currentDispatcher != Dispatchers.IO) { - // on Android, tor startup MUST be run in a background thread, because it is a network operation. - // see [android.os.NetworkOnMainThreadException] - this.start(CoroutineScope(scope.coroutineContext.job + Dispatchers.Default)) - } else { - this.start(scope) - } -} \ No newline at end of file diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/PhoenixBusiness.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/PhoenixBusiness.kt index 578a58f4a..4fc7b20e7 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/PhoenixBusiness.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/PhoenixBusiness.kt @@ -36,7 +36,6 @@ import fr.acinq.phoenix.db.createAppDbDriver import fr.acinq.phoenix.managers.* import fr.acinq.phoenix.utils.* import fr.acinq.phoenix.utils.logger.PhoenixLoggerConfig -import fr.acinq.tor.Tor import io.ktor.client.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.serialization.kotlinx.json.* @@ -57,12 +56,7 @@ class PhoenixBusiness( private val tcpSocketBuilder = TcpSocket.Builder() internal val tcpSocketBuilderFactory = suspend { - val isTorEnabled = appConfigurationManager.isTorEnabled.filterNotNull().first() - if (isTorEnabled) { - tcpSocketBuilder.torProxy(loggerFactory) - } else { - tcpSocketBuilder - } + tcpSocketBuilder } internal val httpClient by lazy { @@ -95,7 +89,6 @@ class PhoenixBusiness( val notificationsManager by lazy { NotificationsManager(this) } val contactsManager by lazy { ContactsManager(this) } val blockchainExplorer by lazy { BlockchainExplorer(chain) } - val tor by lazy { Tor(getApplicationCacheDirectoryPath(ctx), TorHelper.torLogger(loggerFactory)) } val sendManager by lazy { SendManager(this) } fun start(startupParams: StartupParams) { diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/AppConfiguration.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/AppConfiguration.kt index 232480f63..61efb27d1 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/AppConfiguration.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/AppConfiguration.kt @@ -234,7 +234,7 @@ sealed class ElectrumConfig { data class StartupParams( /** When true, we use a [InitTlv] to ask our peer whether there are legacy channels to reestablish for the legacy node id. */ val requestCheckLegacyChannels: Boolean = false, - /** Tor state must be defined before the node starts. */ + /** If true, we'll use onion addresses when connecting to the peer and to Electrum servers. */ val isTorEnabled: Boolean, /** The liquidity policy must be injected into the node params manager. */ val liquidityPolicy: LiquidityPolicy, diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/ElectrumServers.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/ElectrumServers.kt index 34cca0cc3..f6fe14c52 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/ElectrumServers.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/ElectrumServers.kt @@ -10,6 +10,9 @@ private fun electrumServer(host: String, port: Int = 50002): ServerAddress = private fun electrumServer(host: String, port: Int = 50002, publicKey: String): ServerAddress = ServerAddress(host = host, port = port, tls = TcpSocket.TLS.PINNED_PUBLIC_KEY(publicKey)) +private fun electrumServerOnion(host: String, port: Int = 50002): ServerAddress = + ServerAddress(host = host, port = port, tls = TcpSocket.TLS.DISABLED) + val mainnetElectrumServers = listOf( electrumServer(host = "electrum.acinq.co"), electrumServer( @@ -169,4 +172,12 @@ val testnetElectrumServers = listOf( ), ) +val mainnetElectrumServersOnion: List by lazy { TODO() } + +val testnetElectrumServersOnion by lazy { + listOf( + electrumServerOnion(host = "explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion", port = 143) + ) +} + expect fun platformElectrumRegtestConf(): ServerAddress diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/AppConfigurationManager.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/AppConfigurationManager.kt index b007f612a..00876574d 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/AppConfigurationManager.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/AppConfigurationManager.kt @@ -30,7 +30,6 @@ import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds class AppConfigurationManager( - private val appDb: SqliteAppDb, private val httpClient: HttpClient, private val electrumWatcher: ElectrumWatcher, private val chain: Chain, @@ -40,7 +39,6 @@ class AppConfigurationManager( constructor(business: PhoenixBusiness) : this( loggerFactory = business.loggerFactory, chain = business.chain, - appDb = business.appDb, httpClient = business.httpClient, electrumWatcher = business.electrumWatcher, ) @@ -217,9 +215,9 @@ class AppConfigurationManager( } ?: ElectrumConfig.Random } - fun randomElectrumServer() = when (chain) { - Chain.Mainnet -> mainnetElectrumServers.random() - Chain.Testnet3 -> testnetElectrumServers.random() + fun randomElectrumServer(isTorEnabled: Boolean) = when (chain) { + Chain.Mainnet -> if (isTorEnabled) mainnetElectrumServersOnion.random() else mainnetElectrumServers.random() + Chain.Testnet3 -> if (isTorEnabled) testnetElectrumServersOnion.random() else testnetElectrumServers.random() Chain.Testnet4 -> TODO() Chain.Signet -> TODO() Chain.Regtest -> platformElectrumRegtestConf() diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/AppConnectionsDaemon.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/AppConnectionsDaemon.kt index fb0062dfd..1f0dc1d6f 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/AppConnectionsDaemon.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/AppConnectionsDaemon.kt @@ -7,12 +7,10 @@ import fr.acinq.lightning.utils.Connection import fr.acinq.lightning.utils.ServerAddress import fr.acinq.phoenix.PhoenixBusiness import fr.acinq.phoenix.data.ElectrumConfig -import fr.acinq.phoenix.utils.TorHelper.connectionState import fr.acinq.lightning.logging.debug import fr.acinq.lightning.logging.error import fr.acinq.lightning.logging.info -import fr.acinq.tor.Tor -import fr.acinq.tor.TorState +import fr.acinq.phoenix.utils.extensions.isOnion import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ReceiveChannel @@ -35,7 +33,6 @@ class AppConnectionsDaemon( private val currencyManager: CurrencyManager, private val networkMonitor: NetworkMonitor, private val tcpSocketBuilder: suspend () -> TcpSocket.Builder, - private val tor: Tor, private val electrumClient: ElectrumClient, ) : CoroutineScope by MainScope() { @@ -47,7 +44,6 @@ class AppConnectionsDaemon( currencyManager = business.currencyManager, networkMonitor = business.networkMonitor, tcpSocketBuilder = business.tcpSocketBuilderFactory, - tor = business.tor, electrumClient = business.electrumClient ) @@ -55,14 +51,12 @@ class AppConnectionsDaemon( private var peerConnectionJob: Job? = null private var electrumConnectionJob: Job? = null - private var torConnectionJob: Job? = null private var httpControlFlowEnabled: Boolean = false private data class TrafficControl( val walletIsAvailable: Boolean = false, val internetIsAvailable: Boolean = false, val torIsEnabled: Boolean = false, - val torIsAvailable: Boolean = false, /** * Under normal circumstances, the connections are automatically managed based on whether @@ -98,11 +92,7 @@ class AppConnectionsDaemon( /** If a configuration value changes, this value can be incremented to force a disconnection. Only used for Electrum. */ val configVersion: Int = 0 ) { - val canConnect get() = if (walletIsAvailable && internetIsAvailable && disconnectCount <= 0) { - if (torIsEnabled) torIsAvailable else true - } else { - false - } + val canConnect get() = walletIsAvailable && internetIsAvailable && disconnectCount <= 0 fun incrementDisconnectCount(): TrafficControl { val safeInc = disconnectCount.let { if (it == Int.MAX_VALUE) it else it + 1 } @@ -115,11 +105,7 @@ class AppConnectionsDaemon( } override fun toString(): String { - val details = when { - torIsEnabled -> "should_connect=${if (disconnectCount <= 0) "YES" else "NO ($disconnectCount)" } internet=${if (internetIsAvailable) "OK" else "NOK"} tor=${if (torIsAvailable) "OK" else "NOK" }" - else -> "should_connect=${if (disconnectCount <= 0) "YES" else "NO ($disconnectCount)" } internet=${if (internetIsAvailable) "OK" else "NOK"}" - } - return "can_connect=${canConnect.toString().uppercase()} ($details)" + return "can_connect=${canConnect.toString().uppercase()} (should_connect=${if (disconnectCount <= 0) "YES" else "NO ($disconnectCount)" } internet=${if (internetIsAvailable) "OK" else "NOK"} tor=${if (torIsEnabled) "YES" else "NO"})" } } @@ -193,51 +179,6 @@ class AppConnectionsDaemon( } } - // Tor state monitor - launch { - tor.state.collect { - val newValue = it == TorState.RUNNING - logger.debug { "torIsAvailable = $newValue" } - torControlChanges.send { copy(torIsAvailable = newValue) } - peerControlChanges.send { copy(torIsAvailable = newValue) } - electrumControlChanges.send { copy(torIsAvailable = newValue) } - httpApiControlChanges.send { copy(torIsAvailable = newValue) } - } - } - - // Tor - launch { - torControlFlow.collect { - when { - it.internetIsAvailable && it.disconnectCount <= 0 && it.torIsEnabled -> { - if (torConnectionJob == null) { - logger.info { "starting tor" } - torConnectionJob = connectionLoop( - name = "Tor", - statusStateFlow = tor.state.connectionState(this), - ) { - try { - tor.startInProperScope(this) - } catch (t: Throwable) { - logger.error(t) { "tor cannot be started: ${t.message}" } - } - } - } - } - else -> { - torConnectionJob?.let { - logger.info { "shutting down tor" } - it.cancel() - tor.stop() - torConnectionJob = null - // Tor runs it's own process, and needs time to shutdown before restarting. - delay(500) - } - } - } - } - } - // Peer launch { var configVersion = 0 @@ -269,26 +210,19 @@ class AppConnectionsDaemon( ) { connectionAttempt -> peer.socketBuilder = tcpSocketBuilder() try { - val connectTimeout = when { - it.torIsEnabled && connectionAttempt <= 6 -> 15.seconds - it.torIsEnabled -> 30.seconds - connectionAttempt <= 1 -> 1.seconds - connectionAttempt <= 3 -> 2.seconds - connectionAttempt <= 6 -> 4.seconds - connectionAttempt <= 10 -> 7.seconds - else -> 10.seconds - } - val handshakeTimeout = when { - it.torIsEnabled && connectionAttempt <= 6 -> 20.seconds - it.torIsEnabled -> 40.seconds - connectionAttempt <= 1 -> 2.seconds - connectionAttempt <= 3 -> 4.seconds - connectionAttempt <= 6 -> 7.seconds - connectionAttempt <= 10 -> 10.seconds - else -> 15.seconds - } + val (connectTimeout, handshakeTimeout) = when { + connectionAttempt <= 1 -> 1.seconds to 2.seconds + connectionAttempt <= 3 -> 2.seconds to 4.seconds + connectionAttempt <= 6 -> 4.seconds to 7.seconds + connectionAttempt <= 10 -> 7.seconds to 10.seconds + else -> 10.seconds to 15.seconds + }.run { if (it.torIsEnabled) first.times(3) to second.times(3) else this } logger.info { "calling Peer.connect with connect_timeout=$connectTimeout handshake_timeout=$handshakeTimeout" } - peer.connect(connectTimeout = connectTimeout, handshakeTimeout = handshakeTimeout) + if (it.torIsEnabled && !peer.walletParams.trampolineNode.isOnion) { + logger.error { "PEER CONNECTION ABORTED: MUST USE AN ONION ADDRESS" } + } else { + peer.connect(connectTimeout = connectTimeout, handshakeTimeout = handshakeTimeout) + } } catch (e: Exception) { logger.error { "error when connecting to peer: ${e.message ?: e::class.simpleName}" } } @@ -329,7 +263,7 @@ class AppConnectionsDaemon( val electrumServerAddress: ServerAddress? = configurationManager.electrumConfig.value?.let { electrumConfig -> when (electrumConfig) { is ElectrumConfig.Custom -> electrumConfig.server - is ElectrumConfig.Random -> configurationManager.randomElectrumServer() + is ElectrumConfig.Random -> configurationManager.randomElectrumServer(it.torIsEnabled) } } if (electrumServerAddress == null) { @@ -345,7 +279,11 @@ class AppConnectionsDaemon( else -> 20.seconds } logger.info { "calling ElectrumClient.connect to server=$electrumServerAddress with handshake_timeout=$handshakeTimeout" } - electrumClient.connect(electrumServerAddress, tcpSocketBuilder(), timeout = handshakeTimeout) + if (it.torIsEnabled && !electrumServerAddress.isOnion) { + logger.error { "ELECTRUM CONNECTION ABORTED: MUST USE AN ONION ADDRESS" } + } else { + electrumClient.connect(electrumServerAddress, tcpSocketBuilder(), timeout = handshakeTimeout) + } } catch (e: Exception) { logger.error { "error when connecting to electrum: ${e.message ?: e::class.simpleName}"} } @@ -522,6 +460,3 @@ class AppConnectionsDaemon( } } } - -/** The start function must run on a different dispatcher depending on the platform. */ -expect suspend fun Tor.startInProperScope(scope: CoroutineScope) diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/ConnectionsManager.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/ConnectionsManager.kt index 2d8d33879..a4581f23c 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/ConnectionsManager.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/ConnectionsManager.kt @@ -4,24 +4,16 @@ import fr.acinq.lightning.blockchain.electrum.ElectrumClient import fr.acinq.lightning.logging.LoggerFactory import fr.acinq.lightning.utils.Connection import fr.acinq.phoenix.PhoenixBusiness -import fr.acinq.phoenix.utils.TorHelper.connectionState -import fr.acinq.tor.Tor import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import plus +import fr.acinq.phoenix.utils.extensions.plus data class Connections( val internet: Connection = Connection.CLOSED(reason = null), - val tor: Connection = Connection.CLOSED(reason = null), val peer: Connection = Connection.CLOSED(reason = null), val electrum: Connection = Connection.CLOSED(reason = null), - val torEnabled: Boolean = false ) { - val global : Connection get() = if (torEnabled) { - internet + tor + peer + electrum - } else { - internet + peer + electrum - } + val global : Connection get() = internet + peer + electrum } @OptIn(ExperimentalCoroutinesApi::class) @@ -30,17 +22,13 @@ class ConnectionsManager( peerManager: PeerManager, electrumClient: ElectrumClient, networkMonitor: NetworkMonitor, - appConfigurationManager: AppConfigurationManager, - tor: Tor ): CoroutineScope { constructor(business: PhoenixBusiness): this( loggerFactory = business.loggerFactory, peerManager = business.peerManager, electrumClient = business.electrumClient, - networkMonitor = business.networkMonitor, - appConfigurationManager = business.appConfigurationManager, - tor = business.tor + networkMonitor = business.networkMonitor ) val log = loggerFactory.newLogger(this::class) @@ -52,10 +40,8 @@ class ConnectionsManager( combine( peer.connectionState, electrumClient.connectionStatus, - networkMonitor.networkState, - appConfigurationManager.isTorEnabled.filterNotNull(), - tor.state.connectionState(this) - ) { peerState, electrumStatus, internetState, torEnabled, torState -> + networkMonitor.networkState + ) { peerState, electrumStatus, internetState -> Connections( peer = peerState, electrum = electrumStatus.toConnectionState(), @@ -63,8 +49,6 @@ class ConnectionsManager( NetworkState.Available -> Connection.ESTABLISHED NetworkState.NotAvailable -> Connection.CLOSED(reason = null) }, - tor = if (torEnabled) torState else Connection.CLOSED(reason = null), - torEnabled = torEnabled ) } }.stateIn( diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/NodeParamsManager.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/NodeParamsManager.kt index c5080d333..141f9c313 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/NodeParamsManager.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/NodeParamsManager.kt @@ -92,6 +92,7 @@ class NodeParamsManager( val chain = Chain.Testnet3 val trampolineNodeId = PublicKey.fromHex("03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134") val trampolineNodeUri = NodeUri(id = trampolineNodeId, "13.248.222.197", 9735) + val trampolineNodeOnionUri = NodeUri(id = trampolineNodeId, "iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", 9735) const val remoteSwapInXpub = "tpubDAmCFB21J9ExKBRPDcVxSvGs9jtcf8U1wWWbS1xTYmnUsuUHPCoFdCnEGxLE3THSWcQE48GHJnyz8XPbYUivBMbLSMBifFd3G9KmafkM9og" val defaultLiquidityPolicy = LiquidityPolicy.Auto( inboundLiquidityTarget = null, // auto inbound liquidity is disabled (it must be purchased manually) diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/PeerManager.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/PeerManager.kt index 13745adbd..96a4fd66e 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/PeerManager.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/PeerManager.kt @@ -156,7 +156,7 @@ class PeerManager( val startupParams = configurationManager.startupParams.filterNotNull().first() val walletParams = WalletParams( - trampolineNode = NodeParamsManager.trampolineNodeUri, + trampolineNode = if (startupParams.isTorEnabled) NodeParamsManager.trampolineNodeOnionUri else NodeParamsManager.trampolineNodeUri, trampolineFees = listOf( TrampolineFees( feeBase = 4.sat, diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/Socks5Proxy.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/Socks5Proxy.kt deleted file mode 100644 index 43dfc592c..000000000 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/Socks5Proxy.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2022 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.phoenix.utils - -import fr.acinq.lightning.io.TcpSocket -import fr.acinq.lightning.logging.LoggerFactory -import fr.acinq.lightning.logging.info -import fr.acinq.tor.Tor -import fr.acinq.tor.socks.socks5Handshake - -class Socks5Proxy( - private val socketBuilder: TcpSocket.Builder, - loggerFactory: LoggerFactory, - private val proxyHost: String, - private val proxyPort: Int -): TcpSocket.Builder { - - val logger = loggerFactory.newLogger(this::class) - - override suspend fun connect( - host: String, - port: Int, - tls: TcpSocket.TLS, - loggerFactory: LoggerFactory, - ): TcpSocket { - val socket = socketBuilder.connect(proxyHost, proxyPort, TcpSocket.TLS.DISABLED, loggerFactory) - val (cHost, cPort) = socks5Handshake( - destinationHost = host, - destinationPort = port, - receive = { socket.receiveFully(it, offset = 0, length = it.size) }, - send = { socket.send(it, offset = 0, length = it.size, flush = true) } - ) - logger.info { "connected through socks5 to $cHost:$cPort" } - val updatedTls = when (tls) { - is TcpSocket.TLS.TRUSTED_CERTIFICATES -> - TcpSocket.TLS.TRUSTED_CERTIFICATES(tls.expectedHostName ?: host) - else -> tls - } - return socket.startTls(updatedTls) - } -} - -fun TcpSocket.Builder.torProxy( - loggerFactory: LoggerFactory -) = Socks5Proxy( - socketBuilder = this, - loggerFactory = loggerFactory, - proxyHost = Tor.SOCKS_ADDRESS, - proxyPort = Tor.SOCKS_PORT -) diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/TorHelper.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/TorHelper.kt deleted file mode 100644 index d87d3254e..000000000 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/TorHelper.kt +++ /dev/null @@ -1,40 +0,0 @@ -package fr.acinq.phoenix.utils - -import fr.acinq.lightning.utils.Connection -import fr.acinq.lightning.logging.LoggerFactory -import fr.acinq.lightning.logging.debug -import fr.acinq.lightning.logging.error -import fr.acinq.lightning.logging.info -import fr.acinq.lightning.logging.warning -import fr.acinq.tor.Tor -import fr.acinq.tor.TorState -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.stateIn - - -object TorHelper { - fun torLogger(loggerFactory: LoggerFactory): (Tor.LogLevel, String) -> Unit { - val logger = loggerFactory.newLogger("Tor") - return { level, message -> - when (level) { - Tor.LogLevel.DEBUG -> logger.debug { message } - Tor.LogLevel.NOTICE -> logger.info { message } - Tor.LogLevel.WARN -> logger.warning { message } - Tor.LogLevel.ERR -> logger.error { message } - } - } - } - - suspend fun StateFlow.connectionState(scope: CoroutineScope) = flow { - collect { torState -> - val newState = when (torState) { - TorState.STARTING -> Connection.ESTABLISHING - TorState.RUNNING -> Connection.ESTABLISHED - TorState.STOPPED -> Connection.CLOSED(null) - } - emit(newState) - } - }.stateIn(scope) -} \ No newline at end of file diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/ConnectionExtensions.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/ConnectionExtensions.kt index efd48391c..419528849 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/ConnectionExtensions.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/ConnectionExtensions.kt @@ -14,7 +14,11 @@ * limitations under the License. */ +package fr.acinq.phoenix.utils.extensions + +import fr.acinq.lightning.NodeUri import fr.acinq.lightning.utils.Connection +import fr.acinq.lightning.utils.ServerAddress operator fun Connection?.plus(other: Connection?) : Connection = when { @@ -24,3 +28,6 @@ operator fun Connection?.plus(other: Connection?) : Connection = other is Connection.CLOSED -> other else -> this ?: other ?: error("cannot combine connections [$this + $other]") } + +val ServerAddress.isOnion get() = this.host.endsWith(".onion") +val NodeUri.isOnion get() = this.host.endsWith(".onion") \ No newline at end of file diff --git a/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/managers/AppConnectionsDaemon.kt b/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/managers/AppConnectionsDaemon.kt deleted file mode 100644 index e282b8182..000000000 --- a/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/managers/AppConnectionsDaemon.kt +++ /dev/null @@ -1,16 +0,0 @@ -package fr.acinq.phoenix.managers - -import fr.acinq.tor.Tor -import kotlinx.coroutines.* - -@OptIn(ExperimentalStdlibApi::class) -actual suspend fun Tor.startInProperScope(scope: CoroutineScope) { - val currentDispatcher = scope.coroutineContext[CoroutineDispatcher.Key] - if (currentDispatcher != Dispatchers.Main) { - // on iOS, we must run tor operations on the main thread, to prevent issues with frozen objects. - // TODO: remove this once we moved to the new memory model - this.start(CoroutineScope(scope.coroutineContext.job + Dispatchers.Main)) - } else { - this.start(scope) - } -} \ No newline at end of file