diff --git a/Bitkit.xcodeproj/project.pbxproj b/Bitkit.xcodeproj/project.pbxproj index c5ce6136..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; @@ -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..95c9033f 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 @@ -205,11 +213,7 @@ class WalletViewModel: ObservableObject { syncState() - do { - try await lightningService.connectToTrustedPeers() - } 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) @@ -232,6 +236,52 @@ 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) + } 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 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 d9932fde..69d1556c 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) + } } } } @@ -109,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)")