diff --git a/LDKNodeMonday/App/WalletClient.swift b/LDKNodeMonday/App/WalletClient.swift index 4bb464f..bed27b6 100644 --- a/LDKNodeMonday/App/WalletClient.swift +++ b/LDKNodeMonday/App/WalletClient.swift @@ -61,6 +61,7 @@ public class WalletClient { await MainActor.run { self.network = lightningClient.getNetwork() self.server = lightningClient.getServer() + self.lsp = lightningClient.getLsp() // Load LSP from keychain if available if let savedLSPNodeId = try? self.keyClient.getLSP(), @@ -73,6 +74,20 @@ public class WalletClient { self.appState = .wallet } } catch let error { + if let nodeError = error as? NodeError, + case .FeerateEstimationUpdateTimeout = nodeError + { + let warning = handleNodeError(nodeError) + await MainActor.run { + self.appError = NSError( + domain: warning.title, + code: nodeError.hashValue, + userInfo: [NSLocalizedDescriptionKey: warning.detail] + ) + self.appState = .error + } + return + } await MainActor.run { self.appError = error self.appState = .error diff --git a/LDKNodeMonday/Service/Lightning Service/LightningNodeService.swift b/LDKNodeMonday/Service/Lightning Service/LightningNodeService.swift index 9d0790d..6b3c56f 100644 --- a/LDKNodeMonday/Service/Lightning Service/LightningNodeService.swift +++ b/LDKNodeMonday/Service/Lightning Service/LightningNodeService.swift @@ -60,10 +60,10 @@ class LightningNodeService { fatalError("Configuration error: No Esplora servers available for \(network)") } self.server = server + let resolvedLspNodeId = + backupInfo.lspNodeId ?? LightningServiceProvider.see_signet.nodeId self.lsp = - LightningServiceProvider.getByNodeId( - backupInfo.lspNodeId ?? LightningServiceProvider.see_signet.nodeId - ) ?? .see_signet + LightningServiceProvider.getByNodeId(resolvedLspNodeId) ?? .see_signet } else { self.network = .signet self.server = .mutiny_signet @@ -427,6 +427,10 @@ class LightningNodeService { return payments } + func currentLsp() -> LightningServiceProvider { + return lsp + } + func status() -> NodeStatus { let status = ldkNode.status() return status @@ -516,6 +520,7 @@ public struct LightningNodeClient { let listPeers: () -> [PeerDetails] let listChannels: () -> [ChannelDetails] let listPayments: () -> [PaymentDetails] + let getLsp: () -> LightningServiceProvider let status: () -> NodeStatus let deleteWallet: () throws -> Void let getBackupInfo: () throws -> BackupInfo @@ -597,6 +602,7 @@ extension LightningNodeClient { listPeers: { LightningNodeService.shared.listPeers() }, listChannels: { LightningNodeService.shared.listChannels() }, listPayments: { LightningNodeService.shared.listPayments() }, + getLsp: { LightningNodeService.shared.currentLsp() }, status: { LightningNodeService.shared.status() }, deleteWallet: { try LightningNodeService.shared.deleteWallet() }, getBackupInfo: { try LightningNodeService.shared.getBackupInfo() }, @@ -643,6 +649,7 @@ extension LightningNodeClient { listPeers: { [] }, listChannels: { [] }, listPayments: { mockPayments }, + getLsp: { .see_signet }, status: { NodeStatus( isRunning: true, diff --git a/LDKNodeMonday/View Model/Home/Receive/ReceiveViewModel.swift b/LDKNodeMonday/View Model/Home/Receive/ReceiveViewModel.swift index 3a33760..b4e1ad1 100644 --- a/LDKNodeMonday/View Model/Home/Receive/ReceiveViewModel.swift +++ b/LDKNodeMonday/View Model/Home/Receive/ReceiveViewModel.swift @@ -15,6 +15,7 @@ class ReceiveViewModel: ObservableObject { @Published var paymentAddresses: [PaymentAddress?] = [] @Published var addressGenerationStatus = AddressGenerationStatus.generating @Published var receiveViewError: MondayError? + @Published var lightningWarning: MondayError? @Published var networkColor = Color.gray @Published var amountSat: UInt64 = 0 @Published var message: String = "" @@ -33,6 +34,8 @@ class ReceiveViewModel: ObservableObject { func receivePayment(amountSat: UInt64, message: String, expirySecs: UInt32) async { await MainActor.run { self.addressGenerationStatus = .generating + self.receiveViewError = nil + self.lightningWarning = nil } let receiveCapacity = maxReceiveCapacity() let needsJIT = @@ -109,23 +112,45 @@ class ReceiveViewModel: ObservableObject { let needsJIT = amountSat.satsAsMsats > receiveCapacity || (amountSat == 0 && receiveCapacity == 0) - // Always try to generate bolt11 invoice - // The needsJIT flag will handle JIT channel creation when capacity is insufficient - do { - let bolt11Invoice = try await lightningClient.bolt11Payment( - amountSat.satsAsMsats, - Bolt11InvoiceDescription.direct(description: message), //message, - expirySecs, - nil, - needsJIT - ) - let bolt11InvoiceString = bolt11Invoice.description - bolt11PaymentAddress = PaymentAddress( - type: needsJIT ? .bolt11Jit : .bolt11, - address: bolt11InvoiceString + // Always try to generate bolt11 invoice unless we know it will fail (e.g., zero amount with no inbound) + if amountSat == 0 && needsJIT { + debugPrint( + "Skipping Bolt11 generation: zero-amount request with no inbound capacity (needsJIT=true)" ) - } catch { - debugPrint("Error generating Bolt11:", error.localizedDescription) + } else { + do { + let bolt11Invoice = try await lightningClient.bolt11Payment( + amountSat.satsAsMsats, + Bolt11InvoiceDescription.direct(description: message), + expirySecs, + nil, + needsJIT + ) + let bolt11InvoiceString = bolt11Invoice.description + bolt11PaymentAddress = PaymentAddress( + type: needsJIT ? .bolt11Jit : .bolt11, + address: bolt11InvoiceString + ) + } catch let nodeError as NodeError { + let mondayError = handleNodeError(nodeError) + let pendingInbound = lightningClient.listPayments() + .filter { $0.direction == .inbound && $0.status == .pending } + let pendingSummary = pendingInbound.map { + "id=\($0.id), amountMsat=\($0.amountMsat), kind=\(String(describing: $0.kind))" + } + debugPrint( + """ + Error generating Bolt11 (needsJIT=\(needsJIT), amountMsat=\(amountSat.satsAsMsats), message=\(message)): + \(nodeError) + Pending inbound payments: \(pendingSummary) + """ + ) + await MainActor.run { + self.lightningWarning = mondayError + } + } catch { + debugPrint("Error generating Bolt11:", error.localizedDescription) + } } // Unified diff --git a/LDKNodeMonday/View Model/Profile/Danger/SeedViewModel.swift b/LDKNodeMonday/View Model/Profile/Danger/SeedViewModel.swift index fd69d77..5ffdff3 100644 --- a/LDKNodeMonday/View Model/Profile/Danger/SeedViewModel.swift +++ b/LDKNodeMonday/View Model/Profile/Danger/SeedViewModel.swift @@ -16,25 +16,44 @@ class SeedViewModel: ObservableObject { serverURL: EsploraServer.mutiny_signet.url ) @Published var seedViewError: MondayError? - private let lightningClient: LightningNodeClient + private let keyClient: KeyClient + private let lightningClient: LightningNodeClient? - init(lightningClient: LightningNodeClient) { + init(keyClient: KeyClient, lightningClient: LightningNodeClient? = nil) { + self.keyClient = keyClient self.lightningClient = lightningClient } func getSeed() { do { - let seed = try lightningClient.getBackupInfo() - self.seed = seed - } catch let error as NodeError { - let errorString = handleNodeError(error) + let seed = try keyClient.getBackupInfo() DispatchQueue.main.async { - self.seedViewError = .init(title: errorString.title, detail: errorString.detail) + self.seed = seed } } catch { + if let lightningClient { + do { + let seed = try lightningClient.getBackupInfo() + DispatchQueue.main.async { + self.seed = seed + } + return + } catch let nodeError as NodeError { + let errorString = handleNodeError(nodeError) + DispatchQueue.main.async { + self.seedViewError = .init( + title: errorString.title, + detail: errorString.detail + ) + } + return + } catch { + // Fall through to generic error handling below. + } + } DispatchQueue.main.async { self.seedViewError = .init( - title: "Unexpected error", + title: "Recovery phrase unavailable", detail: error.localizedDescription ) } diff --git a/LDKNodeMonday/View/Home/Receive/ReceiveView.swift b/LDKNodeMonday/View/Home/Receive/ReceiveView.swift index e05bc6e..f9f230f 100644 --- a/LDKNodeMonday/View/Home/Receive/ReceiveView.swift +++ b/LDKNodeMonday/View/Home/Receive/ReceiveView.swift @@ -37,6 +37,19 @@ struct ReceiveView: View { selectedPaymentAddress: selectedPaymentAddress, addressArray: viewModel.paymentAddresses.compactMap { $0 } ) + if let warning = viewModel.lightningWarning { + VStack(spacing: 4) { + Text(warning.title) + .font(.subheadline.weight(.semibold)) + .foregroundStyle(.secondary) + Text(warning.detail) + .font(.caption) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + } + .padding(.top, 16) + .padding(.horizontal, 32) + } } Spacer() diff --git a/LDKNodeMonday/View/Settings/Danger/SeedView.swift b/LDKNodeMonday/View/Settings/Danger/SeedView.swift index a8c3bb5..e16e8e9 100644 --- a/LDKNodeMonday/View/Settings/Danger/SeedView.swift +++ b/LDKNodeMonday/View/Settings/Danger/SeedView.swift @@ -10,13 +10,19 @@ import LDKNode import SwiftUI struct SeedView: View { - @ObservedObject var viewModel: SeedViewModel + @StateObject private var viewModel: SeedViewModel @State private var showAlert = false @State private var showRecoveryPhrase = false @State private var isCopied = false @State private var showCheckmark = false @State private var showingSeedViewErrorAlert = false + init(keyClient: KeyClient, lightningClient: LightningNodeClient) { + _viewModel = StateObject( + wrappedValue: SeedViewModel(keyClient: keyClient, lightningClient: lightningClient) + ) + } + var body: some View { VStack(alignment: .center) { @@ -46,7 +52,8 @@ struct SeedView: View { preferredWordsPerRow: 2, usePaging: true, wordsPerPage: 12 - ).padding() + ) + .padding() HStack { Button( @@ -98,6 +105,6 @@ struct SeedView: View { #if DEBUG #Preview { - SeedView(viewModel: .init(lightningClient: .mock)) + SeedView(keyClient: .mock, lightningClient: .mock) } #endif diff --git a/LDKNodeMonday/View/Settings/SettingsView.swift b/LDKNodeMonday/View/Settings/SettingsView.swift index c474960..18ba2c3 100644 --- a/LDKNodeMonday/View/Settings/SettingsView.swift +++ b/LDKNodeMonday/View/Settings/SettingsView.swift @@ -25,7 +25,8 @@ struct SettingsView: View { Section { NavigationLink( destination: SeedView( - viewModel: .init(lightningClient: viewModel.lightningClient) + keyClient: viewModel.keyClient, + lightningClient: viewModel.lightningClient ) ) { Label("Recovery Phrase", systemImage: "lock")