From b77c8125b87a4117f297a853a3d194299fadbe38 Mon Sep 17 00:00:00 2001 From: benk10 Date: Sat, 21 Feb 2026 22:48:48 +0700 Subject: [PATCH 1/4] fix: fetch trusted peer IPs from Blocktank API and update LND4 address --- Bitkit.xcodeproj/project.pbxproj | 6 +-- .../xcshareddata/swiftpm/Package.resolved | 3 +- Bitkit/Constants/Env.swift | 2 +- Bitkit/Services/LightningService.swift | 45 ++++++++++++++++++- Bitkit/ViewModels/WalletViewModel.swift | 25 ++++++++++- 5 files changed, 72 insertions(+), 9 deletions(-) diff --git a/Bitkit.xcodeproj/project.pbxproj b/Bitkit.xcodeproj/project.pbxproj index c5ce6136..390089e5 100644 --- a/Bitkit.xcodeproj/project.pbxproj +++ b/Bitkit.xcodeproj/project.pbxproj @@ -893,7 +893,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/synonymdev/vss-rust-client-ffi"; requirement = { - branch = "master"; + branch = master; kind = branch; }; }; @@ -925,8 +925,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/synonymdev/ldk-node"; requirement = { - branch = main; - kind = branch; + kind = revision; + revision = d2a82a2d111e5eb84a0eec02f4754e39fea4189a; }; }; 96DEA0382DE8BBA1009932BF /* XCRemoteSwiftPackageReference "bitkit-core" */ = { diff --git a/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 840eabd0..d5728d8b 100644 --- a/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -24,8 +24,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/synonymdev/ldk-node", "state" : { - "branch" : "main", - "revision" : "65f616fb466bde34a95c09eb85217eaee176e1e9" + "revision" : "d2a82a2d111e5eb84a0eec02f4754e39fea4189a" } }, { diff --git a/Bitkit/Constants/Env.swift b/Bitkit/Constants/Env.swift index e2a2a967..30ad81e7 100644 --- a/Bitkit/Constants/Env.swift +++ b/Bitkit/Constants/Env.swift @@ -177,7 +177,7 @@ enum Env { return [ .init(nodeId: "039b8b4dd1d88c2c5db374290cda397a8f5d79f312d6ea5d5bfdfc7c6ff363eae3", host: "34.65.111.104", port: 9735), .init(nodeId: "03816141f1dce7782ec32b66a300783b1d436b19777e7c686ed00115bd4b88ff4b", host: "34.65.191.64", port: 9735), - .init(nodeId: "02a371038863605300d0b3fc9de0cf5ccb57728b7f8906535709a831b16e311187", host: "34.65.186.40", port: 9735), + .init(nodeId: "02a371038863605300d0b3fc9de0cf5ccb57728b7f8906535709a831b16e311187", host: "34.65.153.174", port: 9735), ] case .signet: return [] diff --git a/Bitkit/Services/LightningService.swift b/Bitkit/Services/LightningService.swift index b708bfa5..2782a94a 100644 --- a/Bitkit/Services/LightningService.swift +++ b/Bitkit/Services/LightningService.swift @@ -306,13 +306,25 @@ class LightningService { Logger.info("Deleted network graph cache at: \(graphPath.path)") } - func connectToTrustedPeers() async throws { + func connectToTrustedPeers(remotePeers: [LnPeer]? = nil) async throws { guard let node else { throw AppError(serviceError: .nodeNotSetup) } + let peers: [LnPeer] + let usingRemotePeers: Bool + if let remotePeers, !remotePeers.isEmpty { + Logger.info("Using \(remotePeers.count) trusted peers from Blocktank API") + peers = remotePeers + usingRemotePeers = true + } else { + Logger.warn("No remote peers available, falling back to preconfigured env peers") + peers = Env.trustedLnPeers + usingRemotePeers = false + } + try await ServiceQueue.background(.ldk) { - for peer in Env.trustedLnPeers { + for peer in peers { do { try node.connect(nodeId: peer.nodeId, address: peer.address, persist: true) Logger.info("Connected to trusted peer: \(peer.nodeId)") @@ -320,6 +332,35 @@ class LightningService { Logger.error(error, context: "Peer: \(peer.nodeId)") } } + + if usingRemotePeers { + self.verifyTrustedPeersOrFallback(node: node, trustedPeers: peers) + } + } + } + + private func verifyTrustedPeersOrFallback(node: Node, trustedPeers: [LnPeer]) { + let connectedPeerIds = Set(node.listPeers().filter(\.isConnected).map(\.nodeId)) + let trustedConnected = trustedPeers.filter { connectedPeerIds.contains($0.nodeId) }.count + let trustedPeerIds = Set(trustedPeers.map(\.nodeId)) + + if trustedConnected == 0, !trustedPeers.isEmpty { + let fallbackPeers = Env.trustedLnPeers.filter { !trustedPeerIds.contains($0.nodeId) } + if fallbackPeers.isEmpty { + Logger.warn("No trusted peers connected. All preconfigured env peers overlap with API peers (not retrying with stale addresses).") + } else { + Logger.warn("No trusted peers connected, trying \(fallbackPeers.count) preconfigured env peer(s) not in API list") + for peer in fallbackPeers { + do { + try node.connect(nodeId: peer.nodeId, address: peer.address, persist: true) + Logger.info("Connected to fallback peer: \(peer.nodeId)") + } catch { + Logger.error(error, context: "Fallback peer: \(peer.nodeId)") + } + } + } + } else { + Logger.info("Connected to \(trustedConnected)/\(trustedPeers.count) trusted peers") } } diff --git a/Bitkit/ViewModels/WalletViewModel.swift b/Bitkit/ViewModels/WalletViewModel.swift index e6ffeca8..86f5f094 100644 --- a/Bitkit/ViewModels/WalletViewModel.swift +++ b/Bitkit/ViewModels/WalletViewModel.swift @@ -206,7 +206,8 @@ class WalletViewModel: ObservableObject { syncState() do { - try await lightningService.connectToTrustedPeers() + let remotePeers = await fetchTrustedPeersFromBlocktank() + try await lightningService.connectToTrustedPeers(remotePeers: remotePeers) } catch { Logger.error("Failed to connect to trusted peers") } @@ -232,6 +233,28 @@ class WalletViewModel: ObservableObject { } } + private func fetchTrustedPeersFromBlocktank() async -> [LnPeer]? { + var info: IBtInfo? + do { + info = try await coreService.blocktank.info(refresh: true) + } catch { + Logger.warn("Blocktank API refresh failed, trying cache: \(error)") + } + if info == nil { + info = try? await coreService.blocktank.info(refresh: false) + } + guard let nodes = info?.nodes, !nodes.isEmpty else { return nil } + let peers = nodes.compactMap { node -> LnPeer? in + guard let connString = node.connectionStrings.first else { return nil } + let address = connString.contains("@") ? String(connString.split(separator: "@").last ?? "") : connString + let parts = address.split(separator: ":") + guard parts.count == 2, let port = UInt16(parts[1]) else { return nil } + return LnPeer(nodeId: node.pubkey, host: String(parts[0]), port: port) + } + Logger.info("Fetched \(peers.count) trusted peers from Blocktank API") + return peers.isEmpty ? nil : peers + } + func stopLightningNode(clearEventCallback: Bool = false) async throws { nodeLifecycleState = .stopping try await lightningService.stop(clearEventCallback: clearEventCallback) From 9d7947efdcc11ff6c880ee35810ccb9bd13db57b Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sun, 22 Feb 2026 21:37:42 +0100 Subject: [PATCH 2/4] fix(settings): add peer simulation picker to LDK Debug screen Add BlocktankPeerSimulation enum to WalletViewModel with simulation modes (None, API Failure, Unreachable Peers) and a segmented picker in the LDK Debug screen for testing Blocktank peer connection edge cases. Dev Settings is already UI-gated via @AppStorage, so no compile-time guards are needed. Co-Authored-By: Claude Opus 4.6 --- Bitkit/ViewModels/WalletViewModel.swift | 23 ++++++++++++++++++++++ Bitkit/Views/Settings/LdkDebugScreen.swift | 15 ++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/Bitkit/ViewModels/WalletViewModel.swift b/Bitkit/ViewModels/WalletViewModel.swift index 86f5f094..28e58a13 100644 --- a/Bitkit/ViewModels/WalletViewModel.swift +++ b/Bitkit/ViewModels/WalletViewModel.swift @@ -51,6 +51,14 @@ class WalletViewModel: ObservableObject { private let transferService: TransferService private let sheetViewModel: SheetViewModel + enum BlocktankPeerSimulation: String, CaseIterable { + case none = "None" + case apiFailure = "API Failure" + case unreachablePeers = "Unreachable Peers" + } + + static var peerSimulation: BlocktankPeerSimulation = .none + @Published var isRestoringWallet = false @Published var balanceInTransferToSavings: Int = 0 @Published var balanceInTransferToSpending: Int = 0 @@ -234,6 +242,21 @@ class WalletViewModel: ObservableObject { } private func fetchTrustedPeersFromBlocktank() async -> [LnPeer]? { + switch Self.peerSimulation { + case .apiFailure: + Logger.warn("⚠️ [DEBUG] Simulating Blocktank API failure — returning nil") + return nil + case .unreachablePeers: + Logger.warn("⚠️ [DEBUG] Simulating unreachable API peers") + return [ + LnPeer(nodeId: "000000000000000000000000000000000000000000000000000000000000000001", + host: "192.0.2.1", port: 9735), + ] + case .none: + break + } + + var info: IBtInfo? do { info = try await coreService.blocktank.info(refresh: true) diff --git a/Bitkit/Views/Settings/LdkDebugScreen.swift b/Bitkit/Views/Settings/LdkDebugScreen.swift index d9932fde..2dc43561 100644 --- a/Bitkit/Views/Settings/LdkDebugScreen.swift +++ b/Bitkit/Views/Settings/LdkDebugScreen.swift @@ -66,6 +66,21 @@ struct LdkDebugScreen: View { } } } + + // Peer Simulation + VStack(alignment: .leading, spacing: 8) { + CaptionMText("Peer Simulation") + + Picker("Peer Simulation", selection: Binding( + get: { WalletViewModel.peerSimulation }, + set: { WalletViewModel.peerSimulation = $0 } + )) { + ForEach(WalletViewModel.BlocktankPeerSimulation.allCases, id: \.self) { mode in + Text(mode.rawValue).tag(mode) + } + } + .pickerStyle(.segmented) + } } } } From 70c25aa8230f7bc442ebdd12d6410eadce52f359 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 23 Feb 2026 00:11:12 +0100 Subject: [PATCH 3/4] fix(settings): reconnect trusted peers after LDK restart in debug screen Extract reconnectTrustedPeers() from WalletViewModel.start() and call it after lightningService.restart() in LdkDebugScreen so the peer simulation picker actually takes effect on restart. Co-Authored-By: Claude Opus 4.6 --- Bitkit/ViewModels/WalletViewModel.swift | 16 ++++++++++------ Bitkit/Views/Settings/LdkDebugScreen.swift | 1 + 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Bitkit/ViewModels/WalletViewModel.swift b/Bitkit/ViewModels/WalletViewModel.swift index 28e58a13..95c9033f 100644 --- a/Bitkit/ViewModels/WalletViewModel.swift +++ b/Bitkit/ViewModels/WalletViewModel.swift @@ -213,12 +213,7 @@ class WalletViewModel: ObservableObject { syncState() - do { - let remotePeers = await fetchTrustedPeersFromBlocktank() - try await lightningService.connectToTrustedPeers(remotePeers: remotePeers) - } catch { - Logger.error("Failed to connect to trusted peers") - } + await reconnectTrustedPeers() // Migration only: fetch peers from remote backup (once) and persist in ldk-node let peerUris = await MigrationsService.shared.tryFetchMigrationPeersFromBackup(walletIndex: walletIndex) @@ -278,6 +273,15 @@ class WalletViewModel: ObservableObject { return peers.isEmpty ? nil : peers } + func reconnectTrustedPeers() async { + do { + let remotePeers = await fetchTrustedPeersFromBlocktank() + try await lightningService.connectToTrustedPeers(remotePeers: remotePeers) + } catch { + Logger.error("Failed to connect to trusted peers") + } + } + func stopLightningNode(clearEventCallback: Bool = false) async throws { nodeLifecycleState = .stopping try await lightningService.stop(clearEventCallback: clearEventCallback) diff --git a/Bitkit/Views/Settings/LdkDebugScreen.swift b/Bitkit/Views/Settings/LdkDebugScreen.swift index 2dc43561..69d1556c 100644 --- a/Bitkit/Views/Settings/LdkDebugScreen.swift +++ b/Bitkit/Views/Settings/LdkDebugScreen.swift @@ -124,6 +124,7 @@ struct LdkDebugScreen: View { isRestartingNode = true let lightningService = LightningService.shared try await lightningService.restart() + await wallet.reconnectTrustedPeers() app.toast(type: .success, title: "Node Restarted", description: "Node restarted successfully") } catch { Logger.error("Failed to restart node: \(error)") From e5955b112ef199a78e9fa0a8d214c4182e10fa52 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 23 Feb 2026 14:51:36 +0100 Subject: [PATCH 4/4] chore: bump iOS version to 2.0.5 (build 178) Co-Authored-By: Claude Opus 4.6 --- Bitkit.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Bitkit.xcodeproj/project.pbxproj b/Bitkit.xcodeproj/project.pbxproj index 390089e5..502cc1e0 100644 --- a/Bitkit.xcodeproj/project.pbxproj +++ b/Bitkit.xcodeproj/project.pbxproj @@ -498,7 +498,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = BitkitNotification/BitkitNotification.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 177; + CURRENT_PROJECT_VERSION = 178; DEVELOPMENT_TEAM = KYH47R284B; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BitkitNotification/Info.plist; @@ -510,7 +510,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.0.4; + MARKETING_VERSION = 2.0.5; PRODUCT_BUNDLE_IDENTIFIER = to.bitkit.notification; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -526,7 +526,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = BitkitNotification/BitkitNotification.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 177; + CURRENT_PROJECT_VERSION = 178; DEVELOPMENT_TEAM = KYH47R284B; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BitkitNotification/Info.plist; @@ -538,7 +538,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.0.4; + MARKETING_VERSION = 2.0.5; PRODUCT_BUNDLE_IDENTIFIER = to.bitkit.notification; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -672,7 +672,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Bitkit/Bitkit.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 177; + CURRENT_PROJECT_VERSION = 178; DEVELOPMENT_ASSET_PATHS = "\"Bitkit/Preview Content\""; DEVELOPMENT_TEAM = KYH47R284B; ENABLE_HARDENED_RUNTIME = YES; @@ -697,7 +697,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 2.0.4; + MARKETING_VERSION = 2.0.5; PRODUCT_BUNDLE_IDENTIFIER = to.bitkit; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; @@ -715,7 +715,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Bitkit/Bitkit.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 177; + CURRENT_PROJECT_VERSION = 178; DEVELOPMENT_ASSET_PATHS = "\"Bitkit/Preview Content\""; DEVELOPMENT_TEAM = KYH47R284B; ENABLE_HARDENED_RUNTIME = YES; @@ -740,7 +740,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 2.0.4; + MARKETING_VERSION = 2.0.5; PRODUCT_BUNDLE_IDENTIFIER = to.bitkit; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto;