Skip to content

Commit

Permalink
(ios) Import channel (#458)
Browse files Browse the repository at this point in the history
This commit adds the debugging tool to manually import encrypted
channels data into the iOS app. See d97e49a for Android.
  • Loading branch information
robbiehanson authored Nov 2, 2023
1 parent 5e8e0c3 commit e68a960
Show file tree
Hide file tree
Showing 7 changed files with 408 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,8 @@ import fr.acinq.phoenix.android.R
import fr.acinq.phoenix.android.business
import fr.acinq.phoenix.android.components.*
import fr.acinq.phoenix.android.components.feedback.ErrorMessage
import fr.acinq.phoenix.android.components.feedback.SuccessMessage
import fr.acinq.phoenix.android.utils.positiveColor
import shapeless.Succ
import fr.acinq.phoenix.utils.import.ChannelsImportResult

@Composable
fun ImportChannelsData(
Expand Down Expand Up @@ -76,51 +75,54 @@ fun ImportChannelsData(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
val business = business
when (val state = vm.state.value) {
ImportChannelsDataState.Init -> {
Button(
text = stringResource(id = R.string.channelimport_import_button),
icon = R.drawable.ic_restore,
onClick = { vm.importData(dataInput.trim()) },
onClick = { vm.importData(dataInput.trim(), business) },
modifier = Modifier.fillMaxWidth()
)
}
ImportChannelsDataState.Importing -> {
ProgressView(text = stringResource(id = R.string.channelimport_importing),)
}
ImportChannelsDataState.Success -> {
Dialog(
onDismiss = {},
properties = DialogProperties(usePlatformDefaultWidth = false, dismissOnBackPress = false, dismissOnClickOutside = false),
buttons = null,
buttonsTopMargin = 0.dp
) {
Column(
modifier = Modifier.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally
is ImportChannelsDataState.Done -> when (val result = state.result) {
is ChannelsImportResult.Success -> {
Dialog(
onDismiss = {},
properties = DialogProperties(usePlatformDefaultWidth = false, dismissOnBackPress = false, dismissOnClickOutside = false),
buttons = null,
buttonsTopMargin = 0.dp
) {
TextWithIcon(
text = stringResource(id = R.string.channelimport_success),
textStyle = MaterialTheme.typography.body2,
icon = R.drawable.ic_check,
iconTint = positiveColor,
)
Text(text = stringResource(id = R.string.channelimport_success_restart), textAlign = TextAlign.Center)
Column(
modifier = Modifier.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
TextWithIcon(
text = stringResource(id = R.string.channelimport_success),
textStyle = MaterialTheme.typography.body2,
icon = R.drawable.ic_check,
iconTint = positiveColor,
)
Text(text = stringResource(id = R.string.channelimport_success_restart), textAlign = TextAlign.Center)
}
}
}
}
is ImportChannelsDataState.Failure -> {
ErrorMessage(
header = stringResource(id = R.string.channelimport_error_title),
details = when (state) {
is ImportChannelsDataState.Failure.Generic -> state.t.localizedMessage
is ImportChannelsDataState.Failure.MalformedData -> stringResource(id = R.string.channelimport_error_decryption)
is ImportChannelsDataState.Failure.DecryptionError -> stringResource(id = R.string.channelimport_error_decryption)
is ImportChannelsDataState.Failure.UnknownVersion -> stringResource(id = R.string.channelimport_error_unknown_version, state.version)
},
alignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
)
is ChannelsImportResult.Failure -> {
ErrorMessage(
header = stringResource(id = R.string.channelimport_error_title),
details = when (result) {
is ChannelsImportResult.Failure.Generic -> result.error.message
is ChannelsImportResult.Failure.MalformedData -> stringResource(id = R.string.channelimport_error_decryption)
is ChannelsImportResult.Failure.DecryptionError -> stringResource(id = R.string.channelimport_error_decryption)
is ChannelsImportResult.Failure.UnknownVersion -> stringResource(id = R.string.channelimport_error_unknown_version, result.version)
},
alignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,11 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import fr.acinq.bitcoin.ByteVector
import fr.acinq.lightning.channel.states.PersistedChannelState
import fr.acinq.lightning.serialization.Encryption.from
import fr.acinq.lightning.serialization.Serialization
import fr.acinq.lightning.wire.EncryptedChannelData
import fr.acinq.phoenix.PhoenixBusiness
import fr.acinq.phoenix.managers.NodeParamsManager
import fr.acinq.phoenix.managers.PeerManager
import fr.acinq.secp256k1.Hex
import fr.acinq.phoenix.utils.import.ChannelsImportHelper
import fr.acinq.phoenix.utils.import.ChannelsImportResult
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
Expand All @@ -37,56 +34,29 @@ import org.slf4j.LoggerFactory
sealed class ImportChannelsDataState {
object Init : ImportChannelsDataState()
object Importing : ImportChannelsDataState()
object Success : ImportChannelsDataState()
sealed class Failure : ImportChannelsDataState() {
data class Generic(val t: Throwable): Failure()
object MalformedData : Failure()
object DecryptionError : Failure()
data class UnknownVersion(val version: Int) : Failure()
}
data class Done(val result: ChannelsImportResult): ImportChannelsDataState()
}

class ImportChannelsDataViewModel(val peerManager: PeerManager, val nodeParamsManager: NodeParamsManager) : ViewModel() {

private val log = LoggerFactory.getLogger(this::class.java)
val state = mutableStateOf<ImportChannelsDataState>(ImportChannelsDataState.Init)

fun importData(data: String) {
fun importData(data: String, business: PhoenixBusiness) {
if (state.value == ImportChannelsDataState.Importing) return
viewModelScope.launch(
Dispatchers.Default + CoroutineExceptionHandler { _, e ->
log.error("failed to import channels data: ", e)
state.value = ImportChannelsDataState.Failure.Generic(e)
state.value = ImportChannelsDataState.Done(ChannelsImportResult.Failure.Generic(e))
}
) {
state.value = ImportChannelsDataState.Importing
delay(300)
val deserializedData = try {
EncryptedChannelData(ByteVector(Hex.decode(data)))
} catch(e: Exception) {
log.error("failed to deserialize data blob: ", e)
state.value = ImportChannelsDataState.Failure.MalformedData
return@launch
}
PersistedChannelState
.from(nodeParamsManager.nodeParams.value!!.nodePrivateKey, deserializedData)
.onFailure {
log.error("failed to decrypt channel state: ", it)
state.value = ImportChannelsDataState.Failure.DecryptionError
}
.onSuccess { res ->
when (res) {
is Serialization.DeserializationResult.Success -> {
val peer = peerManager.getPeer()
peer.db.channels.addOrUpdateChannel(res.state)
state.value = ImportChannelsDataState.Success
}
is Serialization.DeserializationResult.UnknownVersion -> {
log.error("cannot use channel state: unknown version=${res.version}")
state.value = ImportChannelsDataState.Failure.UnknownVersion(res.version)
}
}
}
val result = ChannelsImportHelper.doImportChannels(
data = data,
biz = business
)
state.value = ImportChannelsDataState.Done(result)
}
}

Expand Down
4 changes: 4 additions & 0 deletions phoenix-ios/phoenix-ios.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@
DCFB8DF72A94066100947698 /* Task+Sleep.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFB8DF62A94066100947698 /* Task+Sleep.swift */; };
DCFB8DF92A94112A00947698 /* Dictionary+MapKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFB8DF82A94112A00947698 /* Dictionary+MapKeys.swift */; };
DCFBC5592AE2CFEF00E3A418 /* BizNotificationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFBC5582AE2CFEF00E3A418 /* BizNotificationCell.swift */; };
DCFBC55B2AEAC2B000E3A418 /* ImportChannelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFBC55A2AEAC2B000E3A418 /* ImportChannelsView.swift */; };
DCFC72042862237400D6B293 /* Asserts.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFC72032862237400D6B293 /* Asserts.swift */; };
DCFD079126D84A380020DD8E /* HorizontalActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFD079026D84A380020DD8E /* HorizontalActivity.swift */; };
F4AED298257A50CD009485C1 /* LogsConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4AED296257A50CD009485C1 /* LogsConfigurationView.swift */; };
Expand Down Expand Up @@ -596,6 +597,7 @@
DCFB8DF62A94066100947698 /* Task+Sleep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Task+Sleep.swift"; sourceTree = "<group>"; };
DCFB8DF82A94112A00947698 /* Dictionary+MapKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+MapKeys.swift"; sourceTree = "<group>"; };
DCFBC5582AE2CFEF00E3A418 /* BizNotificationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BizNotificationCell.swift; sourceTree = "<group>"; };
DCFBC55A2AEAC2B000E3A418 /* ImportChannelsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportChannelsView.swift; sourceTree = "<group>"; };
DCFC72032862237400D6B293 /* Asserts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Asserts.swift; sourceTree = "<group>"; };
DCFD079026D84A380020DD8E /* HorizontalActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalActivity.swift; sourceTree = "<group>"; };
F4AED296257A50CD009485C1 /* LogsConfigurationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogsConfigurationView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1281,6 +1283,7 @@
children = (
53BEF648B3A03C66B611BC06 /* ChannelsConfigurationView.swift */,
DCA5391B29F7202F001BD3D5 /* ChannelInfoPopup.swift */,
DCFBC55A2AEAC2B000E3A418 /* ImportChannelsView.swift */,
);
path = channels;
sourceTree = "<group>";
Expand Down Expand Up @@ -1738,6 +1741,7 @@
DCACF7092566D0F00009B01E /* AppAccessView.swift in Sources */,
DC27E4D1279753EC00C777CC /* TextFieldNumberStyler.swift in Sources */,
DC46CB1628D9F30500C4EAC7 /* LoadingView.swift in Sources */,
DCFBC55B2AEAC2B000E3A418 /* ImportChannelsView.swift in Sources */,
DCFB8DF72A94066100947698 /* Task+Sleep.swift in Sources */,
DC39D4EF287497440030F18D /* SmartModal.swift in Sources */,
DC2F431A27B699800006FCC4 /* ModifyInvoiceSheet.swift in Sources */,
Expand Down
33 changes: 33 additions & 0 deletions phoenix-ios/phoenix-ios/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -2876,6 +2876,9 @@
}
}
}
},
"An unknown error has occurred." : {

},
"An unknown error occurred." : {
"comment" : "error details",
Expand Down Expand Up @@ -6257,6 +6260,12 @@
}
}
}
},
"Data could not be decrypted by this wallet." : {

},
"Data is malformed. An encrypted hex blob is expected." : {

},
"Decrease value" : {
"localizations" : {
Expand Down Expand Up @@ -9832,6 +9841,15 @@
}
}
}
},
"Import channels" : {
"comment" : "Navigation bar title"
},
"Import has failed" : {

},
"Import successful" : {

},
"Important messages" : {
"localizations" : {
Expand All @@ -9854,6 +9872,9 @@
}
}
}
},
"Importing data…" : {

},
"In order to receive this payment, a new payment channel was opened. This is not always required." : {
"comment" : "Fees explanation",
Expand Down Expand Up @@ -13091,6 +13112,9 @@
}
}
}
},
"Paste encrypted data blob here" : {

},
"Paste from clipboard" : {
"localizations" : {
Expand Down Expand Up @@ -18831,6 +18855,9 @@
}
}
}
},
"This screen is a debugging tool that can be used to manually import encrypted channels data.\n\nUse with caution." : {

},
"This seed is invalid and cannot be imported.\n\nPlease try again" : {
"localizations" : {
Expand Down Expand Up @@ -20318,6 +20345,9 @@
}
}
}
},
"Version %d is not supported" : {

},
"via" : {
"comment" : "Label in DetailsView_IncomingPayment",
Expand Down Expand Up @@ -21738,6 +21768,9 @@
}
}
}
},
"You must now restart Phoenix." : {

},
"You scanned a bitcoin address. Phoenix currently only supports sending Lightning payments. You can use a third-party service to make the offchain->onchain swap." : {
"comment" : "Error message - scanning lightning invoice",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ fileprivate struct ChannelsView : View {
@Binding var showChannelsRemoteBalance: Bool
@ObservedObject var toast: Toast

@State var forceCloseChannelsOpen = false
@State var importChannelsOpen = false

@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

Expand All @@ -116,8 +116,8 @@ fileprivate struct ChannelsView : View {
ZStack {
if #unavailable(iOS 16.0) {
NavigationLink(
destination: forceCloseChannelsView(),
isActive: $forceCloseChannelsOpen
destination: importChannelsView(),
isActive: $importChannelsOpen
) {
EmptyView()
}
Expand Down Expand Up @@ -150,29 +150,29 @@ fileprivate struct ChannelsView : View {

} // </ZStack>
.navigationBarItems(trailing: menuButton())
.navigationStackDestination(isPresented: $forceCloseChannelsOpen) { // For iOS 16+
forceCloseChannelsView()
.navigationStackDestination(isPresented: $importChannelsOpen) { // For iOS 16+
importChannelsView()
}
}

@ViewBuilder
func forceCloseChannelsView() -> some View {
ForceCloseChannelsView()
func importChannelsView() -> some View {
ImportChannelsView()
}

@ViewBuilder
func menuButton() -> some View {

Menu {
// Button {
// sharing = mvi.model.json
// } label: {
// Label {
// Text(verbatim: "Share all")
// } icon: {
// Image(systemName: "square.and.arrow.up")
// }
// }
Button {
importChannels()
} label: {
Label {
Text("Import channels")
} icon: {
Image(systemName: "square.and.arrow.down")
}
}
Button {
closeAllChannels()
} label: {
Expand Down Expand Up @@ -218,6 +218,12 @@ fileprivate struct ChannelsView : View {
)
}

func importChannels() {
log.trace("importChannels()")

importChannelsOpen = true
}

func closeAllChannels() {
log.trace("closeAllChannels()")

Expand All @@ -230,7 +236,10 @@ fileprivate struct ChannelsView : View {
func forceCloseAllChannels() {
log.trace("forceCloseAllChannels()")

forceCloseChannelsOpen = true
presentationMode.wrappedValue.dismiss()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.55) {
deepLinkManager.broadcast(.forceCloseChannels)
}
}
}

Expand Down
Loading

0 comments on commit e68a960

Please sign in to comment.