diff --git a/BDKSwiftExampleWallet.xcodeproj/project.pbxproj b/BDKSwiftExampleWallet.xcodeproj/project.pbxproj index 88d5ffbd..90fb16d0 100644 --- a/BDKSwiftExampleWallet.xcodeproj/project.pbxproj +++ b/BDKSwiftExampleWallet.xcodeproj/project.pbxproj @@ -1106,7 +1106,7 @@ repositoryURL = "https://github.com/bitcoindevkit/bdk-swift"; requirement = { kind = exactVersion; - version = 2.0.0; + version = 2.2.0; }; }; AEAF83B42B7BD4D10019B23B /* XCRemoteSwiftPackageReference "CodeScanner" */ = { diff --git a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift index af0464c8..4a5ec154 100644 --- a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift +++ b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift @@ -12,19 +12,26 @@ extension CbfClient { // Track monitoring tasks per client for clean cancellation private static var monitoringTasks: [ObjectIdentifier: Task] = [:] private static var warningTasks: [ObjectIdentifier: Task] = [:] - private static var logTasks: [ObjectIdentifier: Task] = [:] private static var heartbeatTasks: [ObjectIdentifier: Task] = [:] private static var lastInfoAt: [ObjectIdentifier: Date] = [:] private static let monitoringTasksQueue = DispatchQueue(label: "cbf.monitoring.tasks") - static func createComponents(wallet: Wallet) -> (client: CbfClient, node: CbfNode) { + static func createComponents( + wallet: Wallet, + scanType: ScanType, + peers: [Peer] + ) -> (client: CbfClient, node: CbfNode) { do { + let network = wallet.network() + let dataDir = Constants.Config.Kyoto.dbPath + print( + "[Kyoto] Preparing CBF components – network: \(network), dataDir: \(dataDir), peers: \(peers.count), scanType: \(scanType)" + ) let components = try CbfBuilder() - .logLevel(logLevel: .debug) - .scanType(scanType: .sync) - .dataDir(dataDir: Constants.Config.Kyoto.dbPath) - .peers(peers: Constants.Networks.Signet.Regular.kyotoPeers) + .scanType(scanType: scanType) + .dataDir(dataDir: dataDir) + .peers(peers: peers) .build(wallet: wallet) components.node.run() @@ -47,20 +54,20 @@ extension CbfClient { let info = try await self.nextInfo() CbfClient.monitoringTasksQueue.sync { Self.lastInfoAt[id] = Date() } switch info { - case .progress(let progress): + case .progress(let chainHeight, let filtersDownloadedPercent): await MainActor.run { NotificationCenter.default.post( name: NSNotification.Name("KyotoProgressUpdate"), object: nil, - userInfo: ["progress": progress] + userInfo: [ + "progress": filtersDownloadedPercent, + "height": Int(chainHeight), + ] ) - } - case .newChainHeight(let height): - await MainActor.run { NotificationCenter.default.post( name: NSNotification.Name("KyotoChainHeightUpdate"), object: nil, - userInfo: ["height": height] + userInfo: ["height": Int(chainHeight)] ) NotificationCenter.default.post( name: NSNotification.Name("KyotoConnectionUpdate"), @@ -68,13 +75,8 @@ extension CbfClient { userInfo: ["connected": true] ) } - case .stateUpdate(let nodeState): + case .blockReceived(_): await MainActor.run { - NotificationCenter.default.post( - name: NSNotification.Name("KyotoStateUpdate"), - object: nil, - userInfo: ["state": nodeState] - ) NotificationCenter.default.post( name: NSNotification.Name("KyotoConnectionUpdate"), object: nil, @@ -89,8 +91,6 @@ extension CbfClient { userInfo: ["connected": true] ) } - default: - break } } catch is CancellationError { break @@ -149,23 +149,6 @@ extension CbfClient { Self.warningTasks[id] = warnings } - // Log listener for detailed debugging - let logs = Task { [self] in - while true { - if Task.isCancelled { break } - do { - let log = try await self.nextLog() - } catch is CancellationError { - break - } catch { - // ignore - } - } - } - - Self.monitoringTasksQueue.sync { - Self.logTasks[id] = logs - } } func stopBackgroundMonitoring() { @@ -175,7 +158,6 @@ extension CbfClient { task.cancel() if let hb = Self.heartbeatTasks.removeValue(forKey: id) { hb.cancel() } if let wt = Self.warningTasks.removeValue(forKey: id) { wt.cancel() } - if let lt = Self.logTasks.removeValue(forKey: id) { lt.cancel() } Self.lastInfoAt.removeValue(forKey: id) } } @@ -184,11 +166,9 @@ extension CbfClient { Self.monitoringTasksQueue.sync { for (_, task) in Self.monitoringTasks { task.cancel() } for (_, wt) in Self.warningTasks { wt.cancel() } - for (_, lt) in Self.logTasks { lt.cancel() } for (_, hb) in Self.heartbeatTasks { hb.cancel() } Self.monitoringTasks.removeAll() Self.warningTasks.removeAll() - Self.logTasks.removeAll() Self.heartbeatTasks.removeAll() Self.lastInfoAt.removeAll() } diff --git a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift index 6eb7cf02..fd6a19f4 100644 --- a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift +++ b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift @@ -17,7 +17,7 @@ enum BlockchainClientType: String, CaseIterable { struct BlockchainClient { let sync: @Sendable (SyncRequest, UInt64) async throws -> Update let fullScan: @Sendable (FullScanRequest, UInt64, UInt64) async throws -> Update - let broadcast: @Sendable (Transaction) throws -> Void + let broadcast: @Sendable (Transaction) async throws -> Void let getUrl: @Sendable () -> String let getType: @Sendable () -> BlockchainClientType let supportsFullScan: @Sendable () -> Bool = { true } @@ -55,7 +55,24 @@ extension BlockchainClient { try FileManager.default.ensureDirectoryExists(at: Constants.Config.Kyoto.dbDirectoryURL) - let components = CbfClient.createComponents(wallet: wallet) + let scanType: ScanType + if BDKService.shared.needsFullScanOfWallet() { + let addressType = BDKService.shared.getAddressType() + let checkpoint: RecoveryPoint = + addressType == .bip86 ? .taprootActivation : .segwitActivation + scanType = .recovery( + usedScriptIndex: 1000, + checkpoint: checkpoint + ) + } else { + scanType = .sync + } + + let components = CbfClient.createComponents( + wallet: wallet, + scanType: scanType, + peers: Constants.Networks.Signet.Regular.kyotoPeers + ) cbfComponents = components return components } @@ -73,7 +90,7 @@ extension BlockchainClient { }, broadcast: { tx in let components = try getOrCreateComponents() - try components.client.broadcast(transaction: tx) + try await components.client.broadcast(transaction: tx) }, getUrl: { peer }, getType: { .kyoto } @@ -556,7 +573,7 @@ private class BDKService { let isSigned = try wallet.sign(psbt: psbt) if isSigned { let transaction = try psbt.extractTx() - try self.blockchainClient.broadcast(transaction) + try await self.blockchainClient.broadcast(transaction) if self.clientType == .kyoto { let lastSeen = UInt64(Date().timeIntervalSince1970) diff --git a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift index cfed79de..726df404 100644 --- a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift @@ -47,7 +47,6 @@ class WalletViewModel { } var isKyotoConnected: Bool = false var currentBlockHeight: UInt32 = 0 - var kyotoNodeState: NodeState? private var updateProgress: @Sendable (UInt64, UInt64) -> Void { { [weak self] inspected, total in @@ -105,6 +104,9 @@ class WalletViewModel { if self.bdkClient.getClientType() != .kyoto { return } if let progress = notification.userInfo?["progress"] as? Float { self.updateKyotoProgress(progress) + if let height = notification.userInfo?["height"] as? Int { + self.currentBlockHeight = UInt32(max(height, 0)) + } // Consider any progress update as evidence of an active connection // so the UI does not falsely show a red disconnected indicator while syncing. if progress > 0 { @@ -148,8 +150,8 @@ class WalletViewModel { guard let self else { return } // Ignore Kyoto updates unless client type is Kyoto if self.bdkClient.getClientType() != .kyoto { return } - if let height = notification.userInfo?["height"] as? UInt32 { - self.currentBlockHeight = height + if let height = notification.userInfo?["height"] as? Int { + self.currentBlockHeight = UInt32(max(height, 0)) // Receiving chain height implies we have peer connectivity self.isKyotoConnected = true // Ensure UI reflects syncing as soon as we see chain activity @@ -162,23 +164,6 @@ class WalletViewModel { } } } - - NotificationCenter.default.addObserver( - forName: NSNotification.Name("KyotoStateUpdate"), - object: nil, - queue: .main - ) { [weak self] notification in - guard let self else { return } - if self.bdkClient.getClientType() != .kyoto { return } - if let nodeState = notification.userInfo?["state"] as? NodeState { - self.kyotoNodeState = nodeState - if nodeState == .transactionsSynced { - self.walletSyncState = .synced - } else { - self.walletSyncState = .syncing - } - } - } } private func fullScanWithProgress() async { diff --git a/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift b/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift index 3a044537..a43927c7 100644 --- a/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift +++ b/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift @@ -5,7 +5,6 @@ // Created by Rubens Machion on 24/04/25. // -import BitcoinDevKit import SwiftUI struct ActivityHomeHeaderView: View { @@ -18,7 +17,6 @@ struct ActivityHomeHeaderView: View { let isKyotoClient: Bool let isKyotoConnected: Bool let currentBlockHeight: UInt32 - let kyotoNodeState: NodeState? let showAllTransactions: () -> Void @@ -209,25 +207,13 @@ struct ActivityHomeHeaderView: View { extension ActivityHomeHeaderView { fileprivate var kyotoStatusText: String? { - guard isKyotoClient, let kyotoNodeState else { return nil } - // Kyoto's NodeState reflects the next stage it will enter, so describe upcoming work. - switch kyotoNodeState { - case .behind: - // Still acquiring header tips, so call out the header sync explicitly. - return "Getting headers..." - case .headersSynced: - // Kyoto reports this once headers are already finished, so surface the next - // actionable phase the node is entering rather than the completed step. - return "Preparing filters..." - case .filterHeadersSynced: - // Filter headers are ready; actual filter scanning starts next. - return "Scanning filters..." - case .filtersSynced: - // Filters are exhausted; the node now gossips for matching blocks/txs. - return "Fetching matches..." - case .transactionsSynced: - // No further phases—fall back to showing percent + standard synced UI. + guard isKyotoClient else { return nil } + if walletSyncState == .synced || progress >= 100 { return nil } + if progress <= 0 { + return isKyotoConnected ? "Getting headers..." : "Connecting..." + } + return "Scanning filters..." } } diff --git a/BDKSwiftExampleWallet/View/WalletView.swift b/BDKSwiftExampleWallet/View/WalletView.swift index fc8419ec..8df0b1d6 100644 --- a/BDKSwiftExampleWallet/View/WalletView.swift +++ b/BDKSwiftExampleWallet/View/WalletView.swift @@ -51,8 +51,7 @@ struct WalletView: View { needsFullScan: viewModel.needsFullScan, isKyotoClient: viewModel.isKyotoClient, isKyotoConnected: viewModel.isKyotoConnected, - currentBlockHeight: viewModel.currentBlockHeight, - kyotoNodeState: viewModel.kyotoNodeState + currentBlockHeight: viewModel.currentBlockHeight ) { showAllTransactions = true } diff --git a/README.md b/README.md index 4ce35bd5..ff67c071 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ Download the app on [TestFlight](https://testflight.apple.com/join/A3nAuYvZ). ## Build -### BDK 1.0 +### BDK Version -The `main` branch of BDK Swift Example Wallet uses [bdk-swift](https://github.com/bitcoindevkit/bdk-swift) 1.0+. +The `main` branch of BDK Swift Example Wallet uses [bdk-swift](https://github.com/bitcoindevkit/bdk-swift) 2.0+. ## Functionality