Skip to content

Commit 55c6a80

Browse files
authored
fix(settings): roll back electrum connection on failure (#261)
1 parent 9f5e624 commit 55c6a80

File tree

9 files changed

+139
-29
lines changed

9 files changed

+139
-29
lines changed

Bitkit/Constants/Env.swift

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,18 @@ enum Env {
6969

7070
static var electrumServerUrl: String {
7171
if isE2E {
72-
return "127.0.0.1:60001"
72+
return "tcp://127.0.0.1:60001"
7373
}
74+
7475
switch network {
75-
case .regtest:
76-
return "34.65.252.32:18483"
7776
case .bitcoin:
78-
return "35.187.18.233:18484"
79-
case .testnet:
80-
fatalError("Testnet network not implemented")
77+
return "ssl://35.187.18.233:8900"
8178
case .signet:
8279
fatalError("Signet network not implemented")
80+
case .testnet:
81+
return "ssl://electrum.blockstream.info:60002"
82+
case .regtest:
83+
return "tcp://34.65.252.32:18483"
8384
}
8485
}
8586

Bitkit/Models/ElectrumServer.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ struct ElectrumServer: Equatable, Codable {
1414
return "\(host):\(port)"
1515
}
1616

17+
/// Returns the full URL with protocol prefix (tcp:// or ssl://)
18+
var fullUrl: String {
19+
let protocolPrefix = protocolType == .ssl ? "ssl://" : "tcp://"
20+
return "\(protocolPrefix)\(host):\(port)"
21+
}
22+
1723
var portString: String {
1824
return String(port)
1925
}

Bitkit/Services/ElectrumConfigService.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,21 @@ class ElectrumConfigService {
2828
/// Gets the default server parsed from Env.electrumServerUrl
2929
func getDefaultServer() -> ElectrumServer {
3030
let defaultServerUrl = Env.electrumServerUrl
31-
let components = defaultServerUrl.split(separator: ":")
31+
32+
guard defaultServerUrl.hasPrefix("tcp://") || defaultServerUrl.hasPrefix("ssl://") else {
33+
fatalError("Invalid default Electrum server URL format: \(defaultServerUrl). Expected tcp:// or ssl:// prefix.")
34+
}
35+
36+
let protocolType: ElectrumProtocol = defaultServerUrl.hasPrefix("ssl://") ? .ssl : .tcp
37+
let urlWithoutProtocol = String(defaultServerUrl.dropFirst(6)) // Remove "ssl://" or "tcp://"
38+
let components = urlWithoutProtocol.split(separator: ":")
3239

3340
guard components.count >= 2 else {
3441
fatalError("Invalid default Electrum server URL: \(defaultServerUrl)")
3542
}
3643

3744
let host = String(components[0])
3845
let port = String(components[1])
39-
let protocolType = getProtocolForPort(port)
4046

4147
return ElectrumServer(host: host, portString: port, protocolType: protocolType)
4248
}
@@ -45,7 +51,7 @@ class ElectrumConfigService {
4551
func saveServerConfig(_ server: ElectrumServer) {
4652
do {
4753
electrumServerData = try JSONEncoder().encode(server)
48-
Logger.info("Saved Electrum server config: \(server.url) (\(server.protocolType.rawValue))")
54+
Logger.info("Saved Electrum server config: \(server.fullUrl)")
4955
} catch {
5056
Logger.error(error, context: "Failed to encode Electrum server config")
5157
}

Bitkit/Services/LightningService.swift

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,52 @@ class LightningService {
119119
}
120120

121121
// Restart the node with the current configuration
122-
try await setup(walletIndex: currentWalletIndex, electrumServerUrl: electrumServerUrl, rgsServerUrl: rgsServerUrl)
123-
try await start()
122+
do {
123+
try await setup(walletIndex: currentWalletIndex, electrumServerUrl: electrumServerUrl, rgsServerUrl: rgsServerUrl)
124+
try await start()
125+
Logger.info("Node restarted successfully")
126+
} catch {
127+
Logger.warn("Failed ldk-node config change, attempting recovery…")
128+
// Attempt to restart with previous config
129+
// If recovery fails, log it but still throw the original error
130+
do {
131+
try await restartWithPreviousConfig()
132+
} catch {
133+
Logger.error("Recovery attempt also failed: \(error)")
134+
}
135+
// Always re-throw the original error that caused the restart failure
136+
throw error
137+
}
138+
}
139+
140+
/// Restarts the node with the previous stored configuration (recovery method)
141+
/// This is called when a config change fails to restore the node to a working state
142+
private func restartWithPreviousConfig() async throws {
143+
Logger.debug("Stopping node for recovery attempt")
144+
145+
// Stop the current node if it exists
146+
if node != nil {
147+
do {
148+
try await stop()
149+
} catch {
150+
Logger.error("Failed to stop node during recovery: \(error)")
151+
// Clear the node reference anyway
152+
node = nil
153+
try? StateLocker.unlock(.lightning)
154+
}
155+
}
156+
157+
Logger.debug("Starting node with previous config for recovery")
124158

125-
Logger.info("Node restarted successfully")
159+
do {
160+
// Restart with nil URLs to use stored/default configuration
161+
try await setup(walletIndex: currentWalletIndex, electrumServerUrl: nil, rgsServerUrl: nil)
162+
try await start()
163+
Logger.debug("Successfully started node with previous config")
164+
} catch {
165+
Logger.error("Failed starting node with previous config: \(error)")
166+
throw error
167+
}
126168
}
127169

128170
/// Pass onEvent when being used in the background to listen for payments, channels, closes, etc

Bitkit/ViewModels/Extensions/SettingsViewModel+Electrum.swift

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,36 +32,60 @@ extension SettingsViewModel {
3232
return (success: false, host: host, port: port, errorMessage: validationError)
3333
}
3434

35-
// Update current server config immediately after validation
36-
electrumCurrentServer = ElectrumServer(
35+
// Create server config (don't save yet - only save after successful connection)
36+
let serverConfig = ElectrumServer(
3737
host: host,
3838
portString: port,
3939
protocolType: electrumSelectedProtocol
4040
)
4141

4242
do {
43-
// Save the configuration to settings
44-
electrumConfigService.saveServerConfig(electrumCurrentServer)
45-
4643
// Restart the Lightning node with the new Electrum server
4744
let currentRgsUrl = rgsConfigService.getCurrentServerUrl()
4845
try await lightningService.restart(
49-
electrumServerUrl: electrumCurrentServer.url,
46+
electrumServerUrl: serverConfig.fullUrl,
5047
rgsServerUrl: currentRgsUrl.isEmpty ? nil : currentRgsUrl
5148
)
5249

50+
// Wait a bit for the connection to establish and verify it's actually working
51+
try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
52+
53+
// Verify the node is actually running and connected
54+
guard let status = lightningService.status, status.isRunning else {
55+
electrumIsLoading = false
56+
Logger.error("Electrum connection failed: Node is not running after restart")
57+
58+
// Reload form and connection status from actual current server (node may have fallen back to previous server)
59+
let actualServer = electrumConfigService.getCurrentServer()
60+
updateForm(with: actualServer)
61+
electrumCurrentServer = actualServer
62+
// Check if node is actually running with the previous server
63+
electrumIsConnected = lightningService.status?.isRunning == true
64+
65+
return (success: false, host: host, port: port, errorMessage: t("settings__es__server_error_description"))
66+
}
67+
68+
// Only save the configuration after successful connection validation
69+
electrumConfigService.saveServerConfig(serverConfig)
70+
electrumCurrentServer = serverConfig
5371
electrumIsConnected = true
5472
electrumIsLoading = false
5573

56-
Logger.info("Successfully connected to Electrum server: \(electrumCurrentServer.url)")
74+
Logger.info("Successfully connected to Electrum server: \(serverConfig.fullUrl)")
5775

5876
return (success: true, host: host, port: port, errorMessage: nil)
5977
} catch {
60-
electrumIsConnected = false
6178
electrumIsLoading = false
6279

6380
Logger.error(error, context: "Failed to connect to Electrum server")
6481

82+
// Reload form and connection status from actual current server (node may have fallen back to previous server)
83+
let actualServer = electrumConfigService.getCurrentServer()
84+
updateForm(with: actualServer)
85+
electrumCurrentServer = actualServer
86+
// Check if node is actually running with the previous server
87+
electrumIsConnected = lightningService.status?.isRunning == true
88+
6589
return (success: false, host: host, port: port, errorMessage: nil)
6690
}
6791
}
@@ -151,6 +175,20 @@ extension SettingsViewModel {
151175
}
152176

153177
private func parseElectrumScanData(_ data: String) -> ElectrumServer? {
178+
// Handle URLs with tcp:// or ssl:// prefix
179+
if data.hasPrefix("tcp://") || data.hasPrefix("ssl://") {
180+
let protocolType: ElectrumProtocol = data.hasPrefix("ssl://") ? .ssl : .tcp
181+
let urlWithoutProtocol = String(data.dropFirst(6)) // Remove "ssl://" or "tcp://"
182+
let components = urlWithoutProtocol.split(separator: ":")
183+
184+
guard components.count >= 2 else { return nil }
185+
186+
let host = String(components[0])
187+
let port = String(components[1])
188+
189+
return ElectrumServer(host: host, portString: port, protocolType: protocolType)
190+
}
191+
154192
// Handle plain format: host:port or host:port:s (Umbrel format)
155193
if !data.hasPrefix("http://") && !data.hasPrefix("https://") {
156194
let parts = data.split(separator: ":")

Bitkit/ViewModels/Extensions/SettingsViewModel+Rgs.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ extension SettingsViewModel {
2626
rgsConfigService.saveServerUrl(url)
2727

2828
// Restart the Lightning node with the new RGS server
29-
let currentElectrumUrl = electrumConfigService.getCurrentServer().url
29+
let currentElectrumUrl = electrumConfigService.getCurrentServer().fullUrl
3030
try await lightningService.restart(electrumServerUrl: currentElectrumUrl, rgsServerUrl: url.isEmpty ? nil : url)
3131

3232
rgsIsLoading = false

Bitkit/ViewModels/SettingsViewModel.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -344,9 +344,7 @@ class SettingsViewModel: NSObject, ObservableObject {
344344
}
345345
}
346346

347-
let electrumServer = electrumConfigService.getCurrentServer()
348-
let protocolPrefix = electrumServer.protocolType == .ssl ? "ssl://" : "tcp://"
349-
let electrumServerUrl = "\(protocolPrefix)\(electrumServer.url)"
347+
let electrumServerUrl = electrumConfigService.getCurrentServer().fullUrl
350348
if !electrumServerUrl.isEmpty { dict["electrumServer"] = electrumServerUrl }
351349

352350
let rgsServerUrl = rgsConfigService.getCurrentServerUrl()
@@ -377,7 +375,7 @@ class SettingsViewModel: NSObject, ObservableObject {
377375
}
378376

379377
if urlString.hasPrefix("tcp://") || urlString.hasPrefix("ssl://") {
380-
let withoutProtocol = urlString.replacingOccurrences(of: "tcp://", with: "").replacingOccurrences(of: "ssl://", with: "")
378+
let withoutProtocol = String(urlString.dropFirst(6)) // Remove "ssl://" or "tcp://"
381379
let parts = withoutProtocol.split(separator: ":")
382380
guard parts.count >= 2 else { return nil }
383381

Bitkit/ViewModels/WalletViewModel.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ class WalletViewModel: ObservableObject {
100100

101101
syncState()
102102
do {
103-
let electrumServerUrl = electrumConfigService.getCurrentServer().url
103+
let electrumServerUrl = electrumConfigService.getCurrentServer().fullUrl
104104
let rgsServerUrl = rgsConfigService.getCurrentServerUrl()
105105
try await lightningService.setup(
106106
walletIndex: walletIndex,
@@ -472,10 +472,19 @@ class WalletViewModel: ObservableObject {
472472
syncBalances()
473473
}
474474

475-
/// Sync node status and ID only
475+
/// Sync node status, ID and lifecycle state
476476
private func syncNodeStatus() {
477477
nodeStatus = lightningService.status
478478
nodeId = lightningService.nodeId
479+
480+
// Sync lifecycle state based on service status
481+
if let status = lightningService.status {
482+
if status.isRunning && nodeLifecycleState != .running {
483+
nodeLifecycleState = .running
484+
} else if !status.isRunning && nodeLifecycleState == .running {
485+
nodeLifecycleState = .stopped
486+
}
487+
}
479488
}
480489

481490
/// Sync channels and peers only

Bitkit/Views/Settings/Advanced/ElectrumSettingsScreen.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ struct ElectrumSettingsScreen: View {
44
@EnvironmentObject var app: AppViewModel
55
@EnvironmentObject var navigation: NavigationViewModel
66
@EnvironmentObject var settings: SettingsViewModel
7+
@EnvironmentObject var wallet: WalletViewModel
78

89
@FocusState private var isTextFieldFocused: Bool
910

@@ -100,12 +101,17 @@ struct ElectrumSettingsScreen: View {
100101
}
101102
.accessibilityIdentifier("ConnectToHost")
102103
}
103-
.buttonBottomPadding(isFocused: isTextFieldFocused)
104+
.bottomSafeAreaPadding()
104105
}
105106
.frame(minHeight: geometry.size.height)
106-
.bottomSafeAreaPadding()
107+
.contentShape(Rectangle())
108+
.onTapGesture {
109+
isTextFieldFocused = false
110+
}
107111
}
112+
.scrollDismissesKeyboard(.interactively)
108113
}
114+
.ignoresSafeArea(.keyboard, edges: .bottom)
109115
}
110116
.navigationBarHidden(true)
111117
.padding(.horizontal, 16)
@@ -117,13 +123,17 @@ struct ElectrumSettingsScreen: View {
117123
private func onConnect() {
118124
Task {
119125
let result = await settings.connectToElectrumServer()
126+
// Sync wallet state to update node lifecycle state for app status
127+
wallet.syncState()
120128
showToast(result.success, result.host, result.port, result.errorMessage)
121129
}
122130
}
123131

124132
private func onReset() {
125133
Task {
126134
let result = await settings.resetElectrumToDefault()
135+
// Sync wallet state to update node lifecycle state for app status
136+
wallet.syncState()
127137
showToast(result.success, result.host, result.port, result.errorMessage)
128138
}
129139
}

0 commit comments

Comments
 (0)