diff --git a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift index dd559ecf..af0464c8 100644 --- a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift +++ b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift @@ -41,7 +41,6 @@ extension CbfClient { let id = ObjectIdentifier(self) let task = Task { [self] in - var hasEstablishedConnection = false while true { if Task.isCancelled { break } do { @@ -63,25 +62,32 @@ extension CbfClient { object: nil, userInfo: ["height": height] ) - if !hasEstablishedConnection { - hasEstablishedConnection = true - NotificationCenter.default.post( - name: NSNotification.Name("KyotoConnectionUpdate"), - object: nil, - userInfo: ["connected": true] - ) - } + NotificationCenter.default.post( + name: NSNotification.Name("KyotoConnectionUpdate"), + object: nil, + userInfo: ["connected": true] + ) + } + case .stateUpdate(let nodeState): + await MainActor.run { + NotificationCenter.default.post( + name: NSNotification.Name("KyotoStateUpdate"), + object: nil, + userInfo: ["state": nodeState] + ) + NotificationCenter.default.post( + name: NSNotification.Name("KyotoConnectionUpdate"), + object: nil, + userInfo: ["connected": true] + ) } case .connectionsMet, .successfulHandshake: await MainActor.run { - if !hasEstablishedConnection { - hasEstablishedConnection = true - NotificationCenter.default.post( - name: NSNotification.Name("KyotoConnectionUpdate"), - object: nil, - userInfo: ["connected": true] - ) - } + NotificationCenter.default.post( + name: NSNotification.Name("KyotoConnectionUpdate"), + object: nil, + userInfo: ["connected": true] + ) } default: break diff --git a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift index 59aa95cc..6eb7cf02 100644 --- a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift +++ b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift @@ -557,6 +557,16 @@ private class BDKService { if isSigned { let transaction = try psbt.extractTx() try self.blockchainClient.broadcast(transaction) + + if self.clientType == .kyoto { + let lastSeen = UInt64(Date().timeIntervalSince1970) + let unconfirmedTx = UnconfirmedTx(tx: transaction, lastSeen: lastSeen) + wallet.applyUnconfirmedTxs(unconfirmedTxs: [unconfirmedTx]) + guard let persister = self.persister else { + throw WalletError.dbNotFound + } + let _ = try wallet.persist(persister: persister) + } } else { throw WalletError.notSigned } diff --git a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift index be5e2399..cfed79de 100644 --- a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift @@ -47,6 +47,7 @@ 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 @@ -62,12 +63,13 @@ class WalletViewModel { } private var updateKyotoProgress: @Sendable (Float) -> Void { - { [weak self] progress in - DispatchQueue.main.async { - self?.progress = progress - let progressPercent = UInt64(progress) - self?.inspectedScripts = progressPercent - self?.totalScripts = 100 + { [weak self] rawProgress in + DispatchQueue.main.async { [weak self] in + guard let self else { return } + let sanitized = rawProgress.isFinite ? min(max(rawProgress, 0), 100) : 0 + self.progress = sanitized + self.inspectedScripts = UInt64(sanitized) + self.totalScripts = 100 } } } @@ -160,6 +162,23 @@ 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 0f9faa6c..3a044537 100644 --- a/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift +++ b/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift @@ -5,6 +5,7 @@ // Created by Rubens Machion on 24/04/25. // +import BitcoinDevKit import SwiftUI struct ActivityHomeHeaderView: View { @@ -17,6 +18,7 @@ struct ActivityHomeHeaderView: View { let isKyotoClient: Bool let isKyotoConnected: Bool let currentBlockHeight: UInt32 + let kyotoNodeState: NodeState? let showAllTransactions: () -> Void @@ -40,7 +42,13 @@ struct ActivityHomeHeaderView: View { } else if walletSyncState == .syncing { HStack { if isKyotoClient { - if progress < 100.0 { // Kyoto progress is percent + if let status = kyotoStatusText { + Text(status) + .padding(.trailing, -5.0) + .fontWeight(.semibold) + .contentTransition(.opacity) + .transition(.opacity) + } else if progress < 100.0 { // Kyoto progress is percent if currentBlockHeight > 0 { Text("Block \(currentBlockHeight)") .padding(.trailing, -5.0) @@ -198,3 +206,28 @@ 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. + return nil + } + } +} diff --git a/BDKSwiftExampleWallet/View/WalletView.swift b/BDKSwiftExampleWallet/View/WalletView.swift index 8df0b1d6..fc8419ec 100644 --- a/BDKSwiftExampleWallet/View/WalletView.swift +++ b/BDKSwiftExampleWallet/View/WalletView.swift @@ -51,7 +51,8 @@ struct WalletView: View { needsFullScan: viewModel.needsFullScan, isKyotoClient: viewModel.isKyotoClient, isKyotoConnected: viewModel.isKyotoConnected, - currentBlockHeight: viewModel.currentBlockHeight + currentBlockHeight: viewModel.currentBlockHeight, + kyotoNodeState: viewModel.kyotoNodeState ) { showAllTransactions = true }