From 2a912c81b79222d887694a9890fdd05c3bed29fe Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 30 Jul 2025 20:25:07 -0500 Subject: [PATCH 01/40] wip: working i think --- .../project.pbxproj | 4 + .../BDK+Extensions/CbfClient+Extensions.swift | 176 +++++++++++++++++ .../Resources/Localizable.xcstrings | 15 ++ .../Service/BDK Service/BDKService.swift | 125 +++++++++--- .../Utilities/Constants.swift | 182 ++++++++++++++---- .../Activity/TransactionDetailViewModel.swift | 4 +- .../View Model/OnboardingViewModel.swift | 55 +++++- .../Settings/SettingsViewModel.swift | 20 +- .../View Model/WalletViewModel.swift | 46 +++++ .../View/Activity/TransactionListView.swift | 21 +- .../View/Home/ActivityHomeHeaderView.swift | 96 ++++++--- .../View/OnboardingView.swift | 38 ++-- .../View/Settings/SettingsView.swift | 1 - BDKSwiftExampleWallet/View/WalletView.swift | 19 +- 14 files changed, 675 insertions(+), 127 deletions(-) create mode 100644 BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift diff --git a/BDKSwiftExampleWallet.xcodeproj/project.pbxproj b/BDKSwiftExampleWallet.xcodeproj/project.pbxproj index aee50db9..d6c3d0ae 100644 --- a/BDKSwiftExampleWallet.xcodeproj/project.pbxproj +++ b/BDKSwiftExampleWallet.xcodeproj/project.pbxproj @@ -54,6 +54,7 @@ AE4984832A1BBBD7009951E2 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AE4984822A1BBBD7009951E2 /* Preview Assets.xcassets */; }; AE49848D2A1BBBD8009951E2 /* BDKSwiftExampleWalletTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE49848C2A1BBBD8009951E2 /* BDKSwiftExampleWalletTests.swift */; }; AE4984A62A1BBCB8009951E2 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = AE4984A52A1BBCB8009951E2 /* README.md */; }; + AE4D97572E3AFF2500E88A38 /* CbfClient+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE4D97562E3AFF2500E88A38 /* CbfClient+Extensions.swift */; }; AE6715FA2A9A9220005C193F /* BDKSwiftExampleWalletPriceServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE6715F92A9A9220005C193F /* BDKSwiftExampleWalletPriceServiceTests.swift */; }; AE6715FD2A9AC056005C193F /* PriceServiceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE6715FC2A9AC056005C193F /* PriceServiceError.swift */; }; AE6715FF2A9AC066005C193F /* FeeServiceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE6715FE2A9AC066005C193F /* FeeServiceError.swift */; }; @@ -163,6 +164,7 @@ AE4984882A1BBBD8009951E2 /* BDKSwiftExampleWalletTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BDKSwiftExampleWalletTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; AE49848C2A1BBBD8009951E2 /* BDKSwiftExampleWalletTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BDKSwiftExampleWalletTests.swift; sourceTree = ""; }; AE4984A52A1BBCB8009951E2 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + AE4D97562E3AFF2500E88A38 /* CbfClient+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CbfClient+Extensions.swift"; sourceTree = ""; }; AE6474732CE559E000A270C6 /* BDKSwiftExampleWallet.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = BDKSwiftExampleWallet.entitlements; sourceTree = ""; }; AE6715F92A9A9220005C193F /* BDKSwiftExampleWalletPriceServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BDKSwiftExampleWalletPriceServiceTests.swift; sourceTree = ""; }; AE6715FC2A9AC056005C193F /* PriceServiceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceServiceError.swift; sourceTree = ""; }; @@ -572,6 +574,7 @@ isa = PBXGroup; children = ( 77EDA65A2E2A5B3800A5E3AD /* URL+Extensions.swift */, + AE4D97562E3AFF2500E88A38 /* CbfClient+Extensions.swift */, AE97E74C2E315A8F000A407D /* AddressType+Extensions.swift */, 77F0FDC82DA9A93700B30E4F /* Persister+Extensions.swift */, AEE6C74B2ABCB3E200442ADD /* Transaction+Extensions.swift */, @@ -756,6 +759,7 @@ AE73239B2DF9C00F00D9BAE2 /* TxId+Extensions.swift in Sources */, AE1390C72A7DB0AF0098127A /* KeyService.swift in Sources */, AED4CC0A2A1D297600CE1831 /* BDKService.swift in Sources */, + AE4D97572E3AFF2500E88A38 /* CbfClient+Extensions.swift in Sources */, AED4CC102A1D522100CE1831 /* WalletView.swift in Sources */, AE7F67092A7451AA00CED561 /* Price.swift in Sources */, AE184EFC2BFE52C800374362 /* Amount+Extensions.swift in Sources */, diff --git a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift new file mode 100644 index 00000000..d89d24a4 --- /dev/null +++ b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift @@ -0,0 +1,176 @@ +// +// CbfClient+Extensions.swift +// BDKSwiftExampleWallet +// +// Created by Matthew Ramsden on 7/30/25. +// + +import BitcoinDevKit +import Foundation + +extension CbfClient { + static func createComponents(wallet: Wallet) -> (client: CbfClient, node: CbfNode) { + do { + let components = try CbfBuilder() + .logLevel(logLevel: .debug) + .scanType(scanType: .sync) + .dataDir(dataDir: Constants.Config.Kyoto.dbPath) + .peers(peers: Constants.Networks.Signet.Regular.kyotoPeers) + .build(wallet: wallet) + Task { + do { + try await components.node.run() + } catch { + // Kyoto: Failed to start node + } + } + + components.client.startBackgroundMonitoring() + + return (client: components.client, node: components.node) + } catch { + fatalError("Failed to create CBF components: \(error)") + } + } + + func startBackgroundMonitoring() { + Task { + var isConnected = false + while true { + if let log = try? await self.nextLog() { + // Parse specific sync stage messages + if log.contains("Attempting to load headers from the database") { + await MainActor.run { + NotificationCenter.default.post( + name: NSNotification.Name("KyotoProgressUpdate"), + object: nil, + userInfo: ["progress": Float(0.2)] + ) + } + } else if log.contains("]: headers") { + await MainActor.run { + NotificationCenter.default.post( + name: NSNotification.Name("KyotoProgressUpdate"), + object: nil, + userInfo: ["progress": Float(0.4)] + ) + } + } else if log.contains("Chain updated") { + let components = log.components(separatedBy: " ") + if components.count >= 4, + components[0] == "Chain" && components[1] == "updated", + let height = UInt32(components[2]) + { + await MainActor.run { + NotificationCenter.default.post( + name: NSNotification.Name("KyotoChainHeightUpdate"), + object: nil, + userInfo: ["height": height] + ) + } + } + + if !isConnected { + isConnected = true + await MainActor.run { + NotificationCenter.default.post( + name: NSNotification.Name("KyotoConnectionUpdate"), + object: nil, + userInfo: ["connected": true] + ) + } + } + } + + if log.contains("Established an encrypted connection") && !isConnected { + isConnected = true + await MainActor.run { + NotificationCenter.default.post( + name: NSNotification.Name("KyotoConnectionUpdate"), + object: nil, + userInfo: ["connected": true] + ) + } + } + + if log.contains("Need connections") && isConnected { + isConnected = false + await MainActor.run { + NotificationCenter.default.post( + name: NSNotification.Name("KyotoConnectionUpdate"), + object: nil, + userInfo: ["connected": false] + ) + } + } + } + try? await Task.sleep(nanoseconds: 100_000_000) + } + } + + Task { + var hasEstablishedConnection = false + while true { + if let info = try? await self.nextInfo() { + switch info { + case let .progress(progress): + await MainActor.run { + NotificationCenter.default.post( + name: NSNotification.Name("KyotoProgressUpdate"), + object: nil, + userInfo: ["progress": progress] + ) + } + case let .newChainHeight(height): + await MainActor.run { + NotificationCenter.default.post( + name: NSNotification.Name("KyotoChainHeightUpdate"), + object: nil, + userInfo: ["height": height] + ) + + if !hasEstablishedConnection { + hasEstablishedConnection = true + NotificationCenter.default.post( + name: NSNotification.Name("KyotoConnectionUpdate"), + object: nil, + userInfo: ["connected": true] + ) + } + } + case .connectionsMet: + await MainActor.run { + hasEstablishedConnection = true + NotificationCenter.default.post( + name: NSNotification.Name("KyotoConnectionUpdate"), + object: nil, + userInfo: ["connected": true] + ) + } + default: + break + } + } + } + } + + Task { + while true { + if let warning = try? await self.nextWarning() { + switch warning { + case .needConnections: + await MainActor.run { + NotificationCenter.default.post( + name: NSNotification.Name("KyotoConnectionUpdate"), + object: nil, + userInfo: ["connected": false] + ) + } + default: + break + } + } + } + } + } +} diff --git a/BDKSwiftExampleWallet/Resources/Localizable.xcstrings b/BDKSwiftExampleWallet/Resources/Localizable.xcstrings index 295efa1b..98a6f145 100644 --- a/BDKSwiftExampleWallet/Resources/Localizable.xcstrings +++ b/BDKSwiftExampleWallet/Resources/Localizable.xcstrings @@ -467,6 +467,9 @@ } } } + }, + "Block %u" : { + }, "Build Transaction Error" : { "localizations" : { @@ -483,6 +486,9 @@ } } } + }, + "Client" : { + }, "Coldcard Verify Address" : { @@ -623,6 +629,9 @@ } } } + }, + "Esplora" : { + }, "Esplora Server" : { "localizations" : { @@ -733,6 +742,9 @@ } } } + }, + "Kyoto" : { + }, "Navigation Title" : { "extractionState" : "stale", @@ -1049,6 +1061,9 @@ } } } + }, + "Select Client Type" : { + }, "Select Fee" : { "localizations" : { diff --git a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift index 45ca24b5..561ffa82 100644 --- a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift +++ b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift @@ -10,14 +10,14 @@ import Foundation enum BlockchainClientType: String, CaseIterable { case esplora = "esplora" - case kyoto = "kyoto" // future + case kyoto = "kyoto" case electrum = "electrum" // future } struct BlockchainClient { - let sync: @Sendable (SyncRequest, UInt64) throws -> Update - let fullScan: @Sendable (FullScanRequest, UInt64, UInt64) throws -> Update - let broadcast: @Sendable (Transaction) throws -> Void + let sync: @Sendable (SyncRequest, UInt64) async throws -> Update + let fullScan: @Sendable (FullScanRequest, UInt64, UInt64) async throws -> Update + let broadcast: @Sendable (Transaction) async throws -> Void let getUrl: @Sendable () -> String let getType: @Sendable () -> BlockchainClientType let supportsFullScan: @Sendable () -> Bool = { true } @@ -40,6 +40,43 @@ extension BlockchainClient { getType: { .esplora } ) } + + static func kyoto(peer: String) -> Self { + var cbfComponents: (client: CbfClient, node: CbfNode)? = nil + + func getOrCreateComponents() async throws -> (client: CbfClient, node: CbfNode) { + if let existing = cbfComponents { + return existing + } + + guard let wallet = BDKService.shared.wallet else { + throw WalletError.walletNotFound + } + + try FileManager.default.ensureDirectoryExists(at: Constants.Config.Kyoto.dbDirectoryURL) + + let components = CbfClient.createComponents(wallet: wallet) + cbfComponents = components + return components + } + + return Self( + sync: { request, _ in + let components = try await getOrCreateComponents() + return try await components.client.update() + }, + fullScan: { request, stopGap, _ in + let components = try await getOrCreateComponents() + return try await components.client.update() + }, + broadcast: { tx in + let components = try await getOrCreateComponents() + try await components.client.broadcast(transaction: tx) + }, + getUrl: { peer }, + getType: { .kyoto } + ) + } } private class BDKService { @@ -53,7 +90,7 @@ private class BDKService { private var needsFullScan: Bool = false private(set) var network: Network private var blockchainURL: String - private var wallet: Wallet? + internal private(set) var wallet: Wallet? init(keyClient: KeyClient = .live) { self.keyClient = keyClient @@ -63,7 +100,14 @@ private class BDKService { let storedClientType = try? keyClient.getClientType() self.clientType = storedClientType ?? .esplora - self.blockchainURL = (try? keyClient.getEsploraURL()) ?? self.network.url + if self.clientType == .kyoto { + self.blockchainURL = Constants.Config.Kyoto.getDefaultPeer(for: self.network) + } else { + self.blockchainURL = (try? keyClient.getEsploraURL()) ?? "" + if self.blockchainURL.isEmpty { + self.blockchainURL = self.network.url + } + } self.blockchainClient = BlockchainClient.esplora(url: self.blockchainURL) updateBlockchainClient() } @@ -77,15 +121,23 @@ private class BDKService { self.network = newNetwork try? keyClient.saveNetwork(newNetwork.description) - let newURL = newNetwork.url - updateBlockchainURL(newURL) + // Only update URL for Esplora clients, Kyoto uses peer addresses + if self.clientType == .esplora { + let newURL = newNetwork.url + updateBlockchainURL(newURL) + } else if self.clientType == .kyoto { + // For Kyoto, update to the correct peer for the new network + let newPeer = Constants.Config.Kyoto.getDefaultPeer(for: newNetwork) + self.blockchainURL = newPeer + updateBlockchainClient() + } } } func updateBlockchainURL(_ newURL: String) { if newURL != self.blockchainURL { self.blockchainURL = newURL - try? keyClient.saveEsploraURL(newURL) // TODO: Future - saveURL(newURL, for: clientType) + try? keyClient.saveEsploraURL(newURL) updateBlockchainClient() } } @@ -96,12 +148,20 @@ private class BDKService { case .esplora: self.blockchainClient = .esplora(url: self.blockchainURL) case .kyoto: - throw WalletError.backendNotImplemented + if self.network != .signet { + self.clientType = .esplora + self.blockchainClient = .esplora(url: self.blockchainURL) + } else { + let peer = + self.blockchainURL.isEmpty + ? Constants.Config.Kyoto.getDefaultPeer(for: self.network) + : self.blockchainURL + self.blockchainClient = .kyoto(peer: peer) + } case .electrum: throw WalletError.backendNotImplemented } } catch { - // Fallback to esplora if selected backend not implemented self.clientType = .esplora self.blockchainClient = .esplora(url: self.blockchainURL) } @@ -227,8 +287,13 @@ private class BDKService { throw WalletError.dbNotFound } - let savedURL = try? keyClient.getEsploraURL() - let baseUrl = savedURL ?? network.url + let baseUrl: String + if self.clientType == .kyoto { + baseUrl = Constants.Config.Kyoto.getDefaultPeer(for: network) + } else { + let savedURL = try? keyClient.getEsploraURL() + baseUrl = savedURL ?? network.url + } var words12: String if let words = words, !words.isEmpty { @@ -335,9 +400,13 @@ private class BDKService { throw WalletError.dbNotFound } - let savedURL = try? keyClient.getEsploraURL() - - let baseUrl = savedURL ?? network.url + let baseUrl: String + if self.clientType == .kyoto { + baseUrl = Constants.Config.Kyoto.getDefaultPeer(for: network) + } else { + let savedURL = try? keyClient.getEsploraURL() + baseUrl = savedURL ?? network.url + } guard let xpubString = xpub, !xpubString.isEmpty else { throw WalletError.walletNotFound @@ -467,7 +536,7 @@ private class BDKService { amount: amount, feeRate: feeRate ) - try signAndBroadcast(psbt: psbt) + try await signAndBroadcast(psbt: psbt) } func buildTransaction(address: String, amount: UInt64, feeRate: UInt64) throws @@ -486,12 +555,12 @@ private class BDKService { return txBuilder } - private func signAndBroadcast(psbt: Psbt) throws { + private func signAndBroadcast(psbt: Psbt) async throws { guard let wallet = self.wallet else { throw WalletError.walletNotFound } 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) } else { throw WalletError.notSigned } @@ -502,7 +571,7 @@ private class BDKService { let syncRequest = try wallet.startSyncWithRevealedSpks() .inspectSpks(inspector: inspector) .build() - let update = try self.blockchainClient.sync( + let update = try await self.blockchainClient.sync( syncRequest, UInt64(5) ) @@ -521,7 +590,7 @@ private class BDKService { let fullScanRequest = try wallet.startFullScan() .inspectSpksForAllKeychains(inspector: inspector) .build() - let update = try self.blockchainClient.fullScan( + let update = try await self.blockchainClient.fullScan( fullScanRequest, // using https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#address-gap-limit UInt64(20), @@ -594,6 +663,18 @@ extension BDKService { func updateClientType(_ newType: BlockchainClientType) { self.clientType = newType try? keyClient.saveClientType(newType) + + // Update URL to match the new client type + if newType == .kyoto { + self.blockchainURL = Constants.Config.Kyoto.getDefaultPeer(for: self.network) + } else if newType == .esplora { + // Keep existing URL if it's valid for this network, otherwise use default + let defaultEsploraURL = self.network.url + if self.blockchainURL.isEmpty || self.blockchainURL.starts(with: "127.0.0.1") { + self.blockchainURL = defaultEsploraURL + } + } + updateBlockchainClient() } @@ -753,7 +834,7 @@ extension BDKClient { needsFullScan: { true }, setNeedsFullScan: { _ in }, getNetwork: { .signet }, - getEsploraURL: { Constants.Config.EsploraServerURLNetwork.Signet.mutiny }, + getEsploraURL: { Constants.Networks.Signet.Mutiny.esploraServers.first ?? "" }, updateNetwork: { _ in }, updateEsploraURL: { _ in }, getAddressType: { .bip86 }, diff --git a/BDKSwiftExampleWallet/Utilities/Constants.swift b/BDKSwiftExampleWallet/Utilities/Constants.swift index 173014c7..520d942a 100644 --- a/BDKSwiftExampleWallet/Utilities/Constants.swift +++ b/BDKSwiftExampleWallet/Utilities/Constants.swift @@ -10,43 +10,153 @@ import Foundation import SwiftUI struct Constants { - struct Config { - struct EsploraServerURLNetwork { - struct Bitcoin { - private static let blockstream = "https://blockstream.info/api" - private static let mempoolspace = "https://mempool.space/api" - static let allValues = [ - mempoolspace, - blockstream, - ] + struct Networks { + struct Bitcoin { + static let esploraServers = [ + "https://mempool.space/api", + "https://blockstream.info/api", + ] + } + + struct Testnet { + static let esploraServers = [ + "https://mempool.space/testnet/api/", + "https://blockstream.info/testnet/api/", + ] + } + + struct Testnet4 { + static let esploraServers = [ + "https://mempool.space/testnet4/api/" + ] + + enum Faucet: String, CaseIterable { + case mempool = "https://mempool.space/testnet4/faucet" + + var url: URL? { URL(string: self.rawValue) } + + var displayName: String { + switch self { + case .mempool: return "Mempool Faucet" + } + } } - struct Regtest { - private static let local = "http://127.0.0.1:3002" - static let allValues = [ - local + } + + struct Regtest { + static let esploraServers = [ + "http://127.0.0.1:3002" + ] + } + + struct Signet { + struct Regular { + static let esploraServers = [ + "https://mempool.space/signet/api" ] - } - struct Signet { - static let bdk = "http://signet.bitcoindevkit.net" - static let mutiny = "https://mutinynet.com/api" - static let allValues = [ - mutiny, - bdk, + + enum Faucet: String, CaseIterable { + case bublina = "https://signet25.bublina.eu.org/" + case signetfaucet = "https://signetfaucet.com" + + var url: URL? { URL(string: self.rawValue) } + + var displayName: String { + switch self { + case .bublina: return "Bublina Faucet" + case .signetfaucet: return "Signet Faucet" + } + } + } + + static let kyotoPeerStrings = [ + "seed.signet.bitcoin.sprovoost.nl:38333", + "signet-seed.achownodes.xyz:38333", + "vrajjeirttkmnt32wpy3cowmnwr13fkla7hpxc4okr3ysd3kqtzmqd.onion:38333", ] - } - struct Testnet { - static let blockstream = "https://blockstream.info/testnet/api/" - static let mempoolspace = "https://mempool.space/testnet/api/" - static let allValues = [ - mempoolspace, - blockstream, + + static let kyotoPeers = [ + // seed.signet.bitcoin.sprovoost.nl:38333 -> 45.79.52.207:38333 + Peer( + address: IpAddress.fromIpv4(q1: 45, q2: 79, q3: 52, q4: 207), + port: 38333, + v2Transport: false + ), + // signet-seed.achownodes.xyz:38333 -> 192.3.169.35:38333 + Peer( + address: IpAddress.fromIpv4(q1: 192, q2: 3, q3: 169, q4: 35), + port: 38333, + v2Transport: false + ), ] } - struct Testnet4 { - static let mempoolspace = "https://mempool.space/testnet4/api/" - static let allValues = [ - mempoolspace + + struct Mutiny { + static let esploraServers = [ + "https://mutinynet.com/api" ] + + enum Faucet: String, CaseIterable { + case mutiny = "https://faucet.mutinynet.com" + + var url: URL? { URL(string: self.rawValue) } + + var displayName: String { + switch self { + case .mutiny: return "Mutiny Faucet" + } + } + } + } + + // Convenience computed properties for backward compatibility + static var allEsploraServers: [String] { + Mutiny.esploraServers + Regular.esploraServers + } + } + } + + struct Config { + struct Kyoto { + static let dbDirectoryName = "kyoto" + + static var dbDirectoryURL: URL { + URL.walletDataDirectoryURL.appendingPathComponent(dbDirectoryName) + } + + static var dbPath: String { + dbDirectoryURL.path + } + + static func getDefaultPeer(for network: Network) -> String { + switch network { + case .signet: + return Networks.Signet.Regular.kyotoPeerStrings.first + ?? "seed.signet.bitcoin.sprovoost.nl:38333" + default: + // Kyoto only supports Signet for now + return Networks.Signet.Regular.kyotoPeerStrings.first + ?? "seed.signet.bitcoin.sprovoost.nl:38333" + } + } + } + + enum SignetNetwork { + case regular + case custom + + var defaultFaucet: URL? { + switch self { + case .regular: + return Networks.Signet.Regular.Faucet.bublina.url + case .custom: + return Networks.Signet.Mutiny.Faucet.mutiny.url + } + } + + static func from(esploraURL: String) -> SignetNetwork { + return Networks.Signet.Mutiny.esploraServers.contains(esploraURL) + ? .custom : .regular } } } @@ -82,15 +192,15 @@ extension Network { var url: String { switch self { case .bitcoin: - Constants.Config.EsploraServerURLNetwork.Bitcoin.allValues.first ?? "" + Constants.Networks.Bitcoin.esploraServers.first ?? "" case .testnet: - Constants.Config.EsploraServerURLNetwork.Testnet.allValues.first ?? "" + Constants.Networks.Testnet.esploraServers.first ?? "" case .signet: - Constants.Config.EsploraServerURLNetwork.Signet.allValues.first ?? "" + Constants.Networks.Signet.allEsploraServers.first ?? "" case .regtest: - Constants.Config.EsploraServerURLNetwork.Regtest.allValues.first ?? "" + Constants.Networks.Regtest.esploraServers.first ?? "" case .testnet4: - Constants.Config.EsploraServerURLNetwork.Testnet4.allValues.first ?? "" + Constants.Networks.Testnet4.esploraServers.first ?? "" } } } diff --git a/BDKSwiftExampleWallet/View Model/Activity/TransactionDetailViewModel.swift b/BDKSwiftExampleWallet/View Model/Activity/TransactionDetailViewModel.swift index 76b88893..c3c42059 100644 --- a/BDKSwiftExampleWallet/View Model/Activity/TransactionDetailViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/Activity/TransactionDetailViewModel.swift @@ -31,13 +31,13 @@ class TransactionDetailViewModel { switch network { case "signet": - if savedEsploraURL == Constants.Config.EsploraServerURLNetwork.Signet.bdk { + if savedEsploraURL == Constants.Networks.Signet.Regular.esploraServers.first { self.esploraURL = "https://mempool.space/signet" } else { self.esploraURL = "https://mutinynet.com" } case "testnet": - if savedEsploraURL == Constants.Config.EsploraServerURLNetwork.Testnet.blockstream { + if savedEsploraURL == Constants.Networks.Testnet.esploraServers.last { self.esploraURL = "https://blockstream.info/testnet" } else { self.esploraURL = "https://mempool.space/testnet" diff --git a/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift b/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift index 907edfc1..9ac30db2 100644 --- a/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/OnboardingViewModel.swift @@ -9,7 +9,6 @@ import BitcoinDevKit import Foundation import SwiftUI - // Can't make @Observable yet // https://developer.apple.com/forums/thread/731187 // Feature or Bug? @@ -30,21 +29,48 @@ class OnboardingViewModel: ObservableObject { @Published var onboardingViewError: AppError? @Published var selectedNetwork: Network = .signet { didSet { + guard !isInitializing else { return } bdkClient.updateNetwork(selectedNetwork) - selectedURL = availableURLs.first ?? "" - bdkClient.updateEsploraURL(selectedURL) + // If switching away from Signet and Kyoto is selected, switch to Esplora + if selectedNetwork != .signet && selectedClientType == .kyoto { + selectedClientType = .esplora + } + if selectedClientType == .esplora { + selectedURL = availableURLs.first ?? "" + } else if selectedClientType == .kyoto { + // Set to a valid Esplora URL to avoid picker warnings, even though Kyoto won't use it + selectedURL = availableURLs.first ?? "" + } } } @Published var selectedURL: String = "" { didSet { - bdkClient.updateEsploraURL(selectedURL) + guard !isInitializing else { return } + // Only update Esplora URL for Esplora clients + if selectedClientType == .esplora { + bdkClient.updateEsploraURL(selectedURL) + } } } @Published var selectedAddressType: AddressType = .bip86 { didSet { + guard !isInitializing else { return } bdkClient.updateAddressType(selectedAddressType) } } + @Published var selectedClientType: BlockchainClientType = .esplora { + didSet { + guard !isInitializing else { return } + bdkClient.updateClientType(selectedClientType) + // When switching client types, update URL appropriately + if selectedClientType == .kyoto { + // Set to a valid Esplora URL to avoid picker warnings, even though Kyoto won't use it + selectedURL = availableURLs.first ?? "" + } else if selectedClientType == .esplora { + selectedURL = availableURLs.first ?? "" + } + } + } @Published var words: String = "" var wordArray: [String] { if words.hasPrefix("xpub") || words.hasPrefix("tpub") || words.hasPrefix("vpub") { @@ -56,15 +82,15 @@ class OnboardingViewModel: ObservableObject { var availableURLs: [String] { switch selectedNetwork { case .bitcoin: - return Constants.Config.EsploraServerURLNetwork.Bitcoin.allValues + return Constants.Networks.Bitcoin.esploraServers case .testnet: - return Constants.Config.EsploraServerURLNetwork.Testnet.allValues + return Constants.Networks.Testnet.esploraServers case .regtest: - return Constants.Config.EsploraServerURLNetwork.Regtest.allValues + return Constants.Networks.Regtest.esploraServers case .signet: - return Constants.Config.EsploraServerURLNetwork.Signet.allValues + return Constants.Networks.Signet.allEsploraServers case .testnet4: - return Constants.Config.EsploraServerURLNetwork.Testnet4.allValues + return Constants.Networks.Testnet4.esploraServers } } var buttonColor: Color { @@ -82,13 +108,22 @@ class OnboardingViewModel: ObservableObject { } } + private var isInitializing = true + init( bdkClient: BDKClient = .live ) { self.bdkClient = bdkClient + + // Set properties during initialization to avoid didSet side effects self.selectedNetwork = bdkClient.getNetwork() - self.selectedURL = bdkClient.getEsploraURL() self.selectedAddressType = bdkClient.getAddressType() + self.selectedClientType = bdkClient.getClientType() + + // Always set to Esplora URL for UI consistency (Kyoto will use peer internally) + self.selectedURL = bdkClient.getEsploraURL() + + isInitializing = false } func createWallet() { diff --git a/BDKSwiftExampleWallet/View Model/Settings/SettingsViewModel.swift b/BDKSwiftExampleWallet/View Model/Settings/SettingsViewModel.swift index 7446a082..fde0f7f4 100644 --- a/BDKSwiftExampleWallet/View Model/Settings/SettingsViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/Settings/SettingsViewModel.swift @@ -35,11 +35,20 @@ class SettingsViewModel: ObservableObject { ) { self.bdkClient = bdkClient self.network = bdkClient.getNetwork().description - self.esploraURL = bdkClient.getEsploraURL() + self.addressType = bdkClient.getAddressType() + + let clientType = bdkClient.getClientType() + if clientType == .kyoto { + self.esploraURL = "Kyoto (P2P)" + } else { + self.esploraURL = bdkClient.getEsploraURL() + } } func getAddressType() { - self.addressType = bdkClient.getAddressType() + DispatchQueue.main.async { + self.addressType = self.bdkClient.getAddressType() + } } func delete() { @@ -94,6 +103,11 @@ class SettingsViewModel: ObservableObject { } func getEsploraUrl() { - self.esploraURL = bdkClient.getEsploraURL() + let clientType = bdkClient.getClientType() + if clientType == .kyoto { + self.esploraURL = "Kyoto (P2P)" + } else { + self.esploraURL = bdkClient.getEsploraURL() + } } } diff --git a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift index d0e3a42d..1f887a7a 100644 --- a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift @@ -42,6 +42,11 @@ class WalletViewModel { var needsFullScan: Bool { bdkClient.needsFullScan() } + var isKyotoClient: Bool { + bdkClient.getClientType() == .kyoto + } + var isKyotoConnected: Bool = false + var currentBlockHeight: UInt32 = 0 private var updateProgress: @Sendable (UInt64, UInt64) -> Void { { [weak self] inspected, total in @@ -53,6 +58,17 @@ class WalletViewModel { } } + private var updateKyotoProgress: @Sendable (Float) -> Void { + { [weak self] progress in + DispatchQueue.main.async { + self?.progress = progress + let progressPercent = UInt64(progress * 100) + self?.inspectedScripts = progressPercent + self?.totalScripts = 100 + } + } + } + private var updateProgressFullScan: @Sendable (UInt64) -> Void { { [weak self] inspected in DispatchQueue.main.async { @@ -73,6 +89,36 @@ class WalletViewModel { self.priceClient = priceClient self.transactions = transactions self.walletSyncState = walletSyncState + + NotificationCenter.default.addObserver( + forName: NSNotification.Name("KyotoProgressUpdate"), + object: nil, + queue: .main + ) { [weak self] notification in + if let progress = notification.userInfo?["progress"] as? Float { + self?.updateKyotoProgress(progress) + } + } + + NotificationCenter.default.addObserver( + forName: NSNotification.Name("KyotoConnectionUpdate"), + object: nil, + queue: .main + ) { [weak self] notification in + if let connected = notification.userInfo?["connected"] as? Bool { + self?.isKyotoConnected = connected + } + } + + NotificationCenter.default.addObserver( + forName: NSNotification.Name("KyotoChainHeightUpdate"), + object: nil, + queue: .main + ) { [weak self] notification in + if let height = notification.userInfo?["height"] as? UInt32 { + self?.currentBlockHeight = height + } + } } private func fullScanWithProgress() async { diff --git a/BDKSwiftExampleWallet/View/Activity/TransactionListView.swift b/BDKSwiftExampleWallet/View/Activity/TransactionListView.swift index 96c48935..c32bab03 100644 --- a/BDKSwiftExampleWallet/View/Activity/TransactionListView.swift +++ b/BDKSwiftExampleWallet/View/Activity/TransactionListView.swift @@ -31,20 +31,17 @@ struct TransactionListView: View { Text("No Transactions") .font(.subheadline) - let mutinyFaucetURL = URL(string: "https://faucet.mutinynet.com") - let signetFaucetURL = URL(string: "https://signetfaucet.com") + let signetNetwork = Constants.Config.SignetNetwork.from( + esploraURL: viewModel.getEsploraURL() + ) - if let mutinyFaucetURL, - let signetFaucetURL, - viewModel.getNetwork() != Network.testnet.description - && viewModel.getNetwork() != Network.testnet4.description + if viewModel.getNetwork() != Network.testnet.description + && viewModel.getNetwork() != Network.testnet4.description { Button { - UIApplication.shared.open( - viewModel.getEsploraURL() - == Constants.Config.EsploraServerURLNetwork.Signet.mutiny - ? mutinyFaucetURL : signetFaucetURL - ) + if let faucetURL = signetNetwork.defaultFaucet { + UIApplication.shared.open(faucetURL) + } } label: { HStack(spacing: 2) { Text("Get sats from faucet") @@ -57,7 +54,7 @@ struct TransactionListView: View { .buttonStyle(.plain) } - let testnet4FaucetURL = URL(string: "https://mempool.space/testnet4/faucet") + let testnet4FaucetURL = Constants.Networks.Testnet4.Faucet.mempool.url if let testnet4FaucetURL, viewModel.getNetwork() == Network.testnet4.description diff --git a/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift b/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift index 2da5b4f8..6c1c7274 100644 --- a/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift +++ b/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift @@ -14,6 +14,9 @@ struct ActivityHomeHeaderView: View { let inspectedScripts: UInt64 let totalScripts: UInt64 let needsFullScan: Bool + let isKyotoClient: Bool + let isKyotoConnected: Bool + let currentBlockHeight: UInt32 let showAllTransactions: () -> Void @@ -37,28 +40,46 @@ struct ActivityHomeHeaderView: View { } else if walletSyncState == .syncing { HStack { if progress < 1.0 { - Text("\(inspectedScripts)") - .padding(.trailing, -5.0) - .fontWeight(.semibold) - .contentTransition(.numericText()) - .transition(.opacity) + if isKyotoClient { + if currentBlockHeight > 0 { + Text("Block \(currentBlockHeight)") + .padding(.trailing, -5.0) + .fontWeight(.semibold) + .contentTransition(.numericText()) + .transition(.opacity) + } else { + Text("Syncing") + .padding(.trailing, -5.0) + .fontWeight(.semibold) + .contentTransition(.numericText()) + .transition(.opacity) + } + } else { + Text("\(inspectedScripts)") + .padding(.trailing, -5.0) + .fontWeight(.semibold) + .contentTransition(.numericText()) + .transition(.opacity) - Text("/") - .padding(.trailing, -5.0) - .transition(.opacity) - Text("\(totalScripts)") - .contentTransition(.numericText()) - .transition(.opacity) + Text("/") + .padding(.trailing, -5.0) + .transition(.opacity) + Text("\(totalScripts)") + .contentTransition(.numericText()) + .transition(.opacity) + } } - Text( - String( - format: "%.0f%%", - progress * 100 + if !isKyotoClient || (isKyotoClient && progress > 0) { + Text( + String( + format: "%.0f%%", + progress * 100 + ) ) - ) - .contentTransition(.numericText()) - .transition(.opacity) + .contentTransition(.numericText()) + .transition(.opacity) + } } .fontDesign(.monospaced) .foregroundStyle(.secondary) @@ -72,6 +93,9 @@ struct ActivityHomeHeaderView: View { HStack { HStack(spacing: 5) { self.syncImageIndicator() + if isKyotoClient { + self.networkConnectionIndicator() + } } .contentTransition(.symbolEffect(.replace.offUp)) @@ -106,12 +130,21 @@ struct ActivityHomeHeaderView: View { ) case .syncing: - AnyView( - Image(systemName: "slowmo") - .symbolEffect( - .variableColor.cumulative - ) - ) + if isKyotoClient && progress > 0 { + AnyView( + ProgressView(value: Double(progress * 100), total: 100) + .foregroundStyle(.green) + .frame(width: 20) + .animation(.interactiveSpring, value: progress) + ) + } else { + AnyView( + Image(systemName: "slowmo") + .symbolEffect( + .variableColor.cumulative + ) + ) + } case .notStarted: AnyView( @@ -125,4 +158,19 @@ struct ActivityHomeHeaderView: View { ) } } + + @ViewBuilder + private func networkConnectionIndicator() -> some View { + if isKyotoConnected { + AnyView( + Image(systemName: "network") + .foregroundStyle(.green) + ) + } else { + AnyView( + Image(systemName: "network.slash") + .foregroundStyle(.red) + ) + } + } } diff --git a/BDKSwiftExampleWallet/View/OnboardingView.swift b/BDKSwiftExampleWallet/View/OnboardingView.swift index 90d875e7..d4d49909 100644 --- a/BDKSwiftExampleWallet/View/OnboardingView.swift +++ b/BDKSwiftExampleWallet/View/OnboardingView.swift @@ -122,25 +122,39 @@ struct OnboardingView: View { .opacity(animateContent ? 1 : 0) .animation(.easeOut(duration: 0.5).delay(1.5), value: animateContent) - Picker("Esplora Server", selection: $viewModel.selectedURL) { - ForEach(viewModel.availableURLs, id: \.self) { url in - Text( - url.replacingOccurrences( - of: "https://", - with: "" - ).replacingOccurrences( - of: "http://", - with: "" - ) - ) - .tag(url) + Picker("Client", selection: $viewModel.selectedClientType) { + Text("Esplora").tag(BlockchainClientType.esplora) + if viewModel.selectedNetwork == .signet { + Text("Kyoto").tag(BlockchainClientType.kyoto) } } .pickerStyle(.automatic) .tint(.primary) + .accessibilityLabel("Select Client Type") .opacity(animateContent ? 1 : 0) .animation(.easeOut(duration: 0.5).delay(1.5), value: animateContent) + if viewModel.selectedClientType == .esplora { + Picker("Esplora Server", selection: $viewModel.selectedURL) { + ForEach(viewModel.availableURLs, id: \.self) { url in + Text( + url.replacingOccurrences( + of: "https://", + with: "" + ).replacingOccurrences( + of: "http://", + with: "" + ) + ) + .tag(url) + } + } + .pickerStyle(.automatic) + .tint(.primary) + .opacity(animateContent ? 1 : 0) + .animation(.easeOut(duration: 0.5).delay(1.5), value: animateContent) + } + Picker("Address Type", selection: $viewModel.selectedAddressType) { ForEach(AddressType.allCases, id: \.self) { type in Text(type.displayName).tag(type) diff --git a/BDKSwiftExampleWallet/View/Settings/SettingsView.swift b/BDKSwiftExampleWallet/View/Settings/SettingsView.swift index e6fdb707..167a445b 100644 --- a/BDKSwiftExampleWallet/View/Settings/SettingsView.swift +++ b/BDKSwiftExampleWallet/View/Settings/SettingsView.swift @@ -118,7 +118,6 @@ struct SettingsView: View { .onAppear { viewModel.getNetwork() viewModel.getEsploraUrl() - viewModel.getAddressType() } .padding(.top, 40.0) diff --git a/BDKSwiftExampleWallet/View/WalletView.swift b/BDKSwiftExampleWallet/View/WalletView.swift index 010a6223..629b3223 100644 --- a/BDKSwiftExampleWallet/View/WalletView.swift +++ b/BDKSwiftExampleWallet/View/WalletView.swift @@ -47,7 +47,10 @@ struct WalletView: View { progress: viewModel.progress, inspectedScripts: viewModel.inspectedScripts, totalScripts: viewModel.totalScripts, - needsFullScan: viewModel.needsFullScan + needsFullScan: viewModel.needsFullScan, + isKyotoClient: viewModel.isKyotoClient, + isKyotoConnected: viewModel.isKyotoConnected, + currentBlockHeight: viewModel.currentBlockHeight ) { showAllTransactions = true } @@ -58,10 +61,16 @@ struct WalletView: View { walletSyncState: viewModel.walletSyncState ) .refreshable { - await viewModel.syncOrFullScan() - viewModel.getBalance() - viewModel.getTransactions() - await viewModel.getPrices() + if viewModel.isKyotoClient { + viewModel.getBalance() + viewModel.getTransactions() + await viewModel.getPrices() + } else { + await viewModel.syncOrFullScan() + viewModel.getBalance() + viewModel.getTransactions() + await viewModel.getPrices() + } } HStack { From 8718349c4a087da991acf702788b21974f84d458 Mon Sep 17 00:00:00 2001 From: Matthew Date: Tue, 5 Aug 2025 10:55:35 -0500 Subject: [PATCH 02/40] fix(untested): show block height immediately --- .../View/Home/ActivityHomeHeaderView.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift b/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift index 6c1c7274..f064fd1a 100644 --- a/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift +++ b/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift @@ -88,6 +88,12 @@ struct ActivityHomeHeaderView: View { .animation(.easeInOut, value: inspectedScripts) .animation(.easeInOut, value: totalScripts) .animation(.easeInOut, value: progress) + } else if walletSyncState == .synced && isKyotoClient && currentBlockHeight > 0 { + Text("Block \(currentBlockHeight)") + .padding(.trailing, -5.0) + .fontWeight(.semibold) + .contentTransition(.numericText()) + .transition(.opacity) } } HStack { From c967bd6bdcec631f94e11a6abb7e7b1365f9c267 Mon Sep 17 00:00:00 2001 From: Matthew Date: Tue, 5 Aug 2025 11:21:46 -0500 Subject: [PATCH 03/40] refactor: broadcast sync --- .../Service/BDK Service/BDKService.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift index 561ffa82..2e2ce51f 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) async throws -> Void + let broadcast: @Sendable (Transaction) throws -> Void let getUrl: @Sendable () -> String let getType: @Sendable () -> BlockchainClientType let supportsFullScan: @Sendable () -> Bool = { true } @@ -44,7 +44,7 @@ extension BlockchainClient { static func kyoto(peer: String) -> Self { var cbfComponents: (client: CbfClient, node: CbfNode)? = nil - func getOrCreateComponents() async throws -> (client: CbfClient, node: CbfNode) { + func getOrCreateComponents() throws -> (client: CbfClient, node: CbfNode) { if let existing = cbfComponents { return existing } @@ -62,16 +62,16 @@ extension BlockchainClient { return Self( sync: { request, _ in - let components = try await getOrCreateComponents() + let components = try getOrCreateComponents() return try await components.client.update() }, fullScan: { request, stopGap, _ in - let components = try await getOrCreateComponents() + let components = try getOrCreateComponents() return try await components.client.update() }, broadcast: { tx in - let components = try await getOrCreateComponents() - try await components.client.broadcast(transaction: tx) + let components = try getOrCreateComponents() + try components.client.broadcast(transaction: tx) }, getUrl: { peer }, getType: { .kyoto } @@ -560,7 +560,7 @@ private class BDKService { let isSigned = try wallet.sign(psbt: psbt) if isSigned { let transaction = try psbt.extractTx() - try await self.blockchainClient.broadcast(transaction) + try self.blockchainClient.broadcast(transaction) } else { throw WalletError.notSigned } From 381d36c0cbfc2fb6fa9c355d247e41790cc0f559 Mon Sep 17 00:00:00 2001 From: Matthew Date: Tue, 5 Aug 2025 11:31:53 -0500 Subject: [PATCH 04/40] refactor: remove log parsing --- .../BDK+Extensions/CbfClient+Extensions.swift | 73 ------------------- 1 file changed, 73 deletions(-) diff --git a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift index d89d24a4..f5baa102 100644 --- a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift +++ b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift @@ -34,79 +34,6 @@ extension CbfClient { } func startBackgroundMonitoring() { - Task { - var isConnected = false - while true { - if let log = try? await self.nextLog() { - // Parse specific sync stage messages - if log.contains("Attempting to load headers from the database") { - await MainActor.run { - NotificationCenter.default.post( - name: NSNotification.Name("KyotoProgressUpdate"), - object: nil, - userInfo: ["progress": Float(0.2)] - ) - } - } else if log.contains("]: headers") { - await MainActor.run { - NotificationCenter.default.post( - name: NSNotification.Name("KyotoProgressUpdate"), - object: nil, - userInfo: ["progress": Float(0.4)] - ) - } - } else if log.contains("Chain updated") { - let components = log.components(separatedBy: " ") - if components.count >= 4, - components[0] == "Chain" && components[1] == "updated", - let height = UInt32(components[2]) - { - await MainActor.run { - NotificationCenter.default.post( - name: NSNotification.Name("KyotoChainHeightUpdate"), - object: nil, - userInfo: ["height": height] - ) - } - } - - if !isConnected { - isConnected = true - await MainActor.run { - NotificationCenter.default.post( - name: NSNotification.Name("KyotoConnectionUpdate"), - object: nil, - userInfo: ["connected": true] - ) - } - } - } - - if log.contains("Established an encrypted connection") && !isConnected { - isConnected = true - await MainActor.run { - NotificationCenter.default.post( - name: NSNotification.Name("KyotoConnectionUpdate"), - object: nil, - userInfo: ["connected": true] - ) - } - } - - if log.contains("Need connections") && isConnected { - isConnected = false - await MainActor.run { - NotificationCenter.default.post( - name: NSNotification.Name("KyotoConnectionUpdate"), - object: nil, - userInfo: ["connected": false] - ) - } - } - } - try? await Task.sleep(nanoseconds: 100_000_000) - } - } Task { var hasEstablishedConnection = false From 6e4c2d081449a3b1b1b046e88f879a3daa108e18 Mon Sep 17 00:00:00 2001 From: Matthew Date: Tue, 5 Aug 2025 12:00:00 -0500 Subject: [PATCH 05/40] Revert "refactor: remove log parsing" This reverts commit 381d36c0cbfc2fb6fa9c355d247e41790cc0f559. --- .../BDK+Extensions/CbfClient+Extensions.swift | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift index f5baa102..d89d24a4 100644 --- a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift +++ b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift @@ -34,6 +34,79 @@ extension CbfClient { } func startBackgroundMonitoring() { + Task { + var isConnected = false + while true { + if let log = try? await self.nextLog() { + // Parse specific sync stage messages + if log.contains("Attempting to load headers from the database") { + await MainActor.run { + NotificationCenter.default.post( + name: NSNotification.Name("KyotoProgressUpdate"), + object: nil, + userInfo: ["progress": Float(0.2)] + ) + } + } else if log.contains("]: headers") { + await MainActor.run { + NotificationCenter.default.post( + name: NSNotification.Name("KyotoProgressUpdate"), + object: nil, + userInfo: ["progress": Float(0.4)] + ) + } + } else if log.contains("Chain updated") { + let components = log.components(separatedBy: " ") + if components.count >= 4, + components[0] == "Chain" && components[1] == "updated", + let height = UInt32(components[2]) + { + await MainActor.run { + NotificationCenter.default.post( + name: NSNotification.Name("KyotoChainHeightUpdate"), + object: nil, + userInfo: ["height": height] + ) + } + } + + if !isConnected { + isConnected = true + await MainActor.run { + NotificationCenter.default.post( + name: NSNotification.Name("KyotoConnectionUpdate"), + object: nil, + userInfo: ["connected": true] + ) + } + } + } + + if log.contains("Established an encrypted connection") && !isConnected { + isConnected = true + await MainActor.run { + NotificationCenter.default.post( + name: NSNotification.Name("KyotoConnectionUpdate"), + object: nil, + userInfo: ["connected": true] + ) + } + } + + if log.contains("Need connections") && isConnected { + isConnected = false + await MainActor.run { + NotificationCenter.default.post( + name: NSNotification.Name("KyotoConnectionUpdate"), + object: nil, + userInfo: ["connected": false] + ) + } + } + } + try? await Task.sleep(nanoseconds: 100_000_000) + } + } Task { var hasEstablishedConnection = false From 4b7c35473b096e817b7fc3c118037c9e160e29c0 Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 6 Aug 2025 09:47:33 -0500 Subject: [PATCH 06/40] `This does not throw` --- .../Extensions/BDK+Extensions/CbfClient+Extensions.swift | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift index d89d24a4..2240abef 100644 --- a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift +++ b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift @@ -17,14 +17,8 @@ extension CbfClient { .dataDir(dataDir: Constants.Config.Kyoto.dbPath) .peers(peers: Constants.Networks.Signet.Regular.kyotoPeers) .build(wallet: wallet) - Task { - do { - try await components.node.run() - } catch { - // Kyoto: Failed to start node - } - } + components.node.run() components.client.startBackgroundMonitoring() return (client: components.client, node: components.node) From 99b85aec7dce7ac9916a0713780ea723814efc71 Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 6 Aug 2025 09:49:28 -0500 Subject: [PATCH 07/40] `Why is this here` --- .../Extensions/BDK+Extensions/CbfClient+Extensions.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift index 2240abef..64f10f26 100644 --- a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift +++ b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift @@ -98,7 +98,7 @@ extension CbfClient { } } } - try? await Task.sleep(nanoseconds: 100_000_000) + } } From 08e6b1276ec873c8656614a8b3fdabd9f9606f4b Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 6 Aug 2025 15:46:13 -0500 Subject: [PATCH 08/40] use `Info::NewChainHeight` --- .../BDK+Extensions/CbfClient+Extensions.swift | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift index 64f10f26..2f6002e8 100644 --- a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift +++ b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift @@ -41,39 +41,6 @@ extension CbfClient { userInfo: ["progress": Float(0.2)] ) } - } else if log.contains("]: headers") { - await MainActor.run { - NotificationCenter.default.post( - name: NSNotification.Name("KyotoProgressUpdate"), - object: nil, - userInfo: ["progress": Float(0.4)] - ) - } - } else if log.contains("Chain updated") { - let components = log.components(separatedBy: " ") - if components.count >= 4, - components[0] == "Chain" && components[1] == "updated", - let height = UInt32(components[2]) - { - await MainActor.run { - NotificationCenter.default.post( - name: NSNotification.Name("KyotoChainHeightUpdate"), - object: nil, - userInfo: ["height": height] - ) - } - } - - if !isConnected { - isConnected = true - await MainActor.run { - NotificationCenter.default.post( - name: NSNotification.Name("KyotoConnectionUpdate"), - object: nil, - userInfo: ["connected": true] - ) - } - } } if log.contains("Established an encrypted connection") && !isConnected { From 3c4ed6f38aa1fda25e75b1b4f1c7974f27085aee Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 6 Aug 2025 15:53:19 -0500 Subject: [PATCH 09/40] Covered by `Info::SuccessfulHandshake` --- .../BDK+Extensions/CbfClient+Extensions.swift | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift index 2f6002e8..6edf331d 100644 --- a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift +++ b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift @@ -43,17 +43,6 @@ extension CbfClient { } } - if log.contains("Established an encrypted connection") && !isConnected { - isConnected = true - await MainActor.run { - NotificationCenter.default.post( - name: NSNotification.Name("KyotoConnectionUpdate"), - object: nil, - userInfo: ["connected": true] - ) - } - } - if log.contains("Need connections") && isConnected { isConnected = false await MainActor.run { @@ -108,6 +97,17 @@ extension CbfClient { userInfo: ["connected": true] ) } + case .successfulHandshake: + await MainActor.run { + if !hasEstablishedConnection { + hasEstablishedConnection = true + NotificationCenter.default.post( + name: NSNotification.Name("KyotoConnectionUpdate"), + object: nil, + userInfo: ["connected": true] + ) + } + } default: break } From b4db0aefad864ba7d9965346d125050e5e57af5b Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 6 Aug 2025 15:56:59 -0500 Subject: [PATCH 10/40] This is a warning condition `Warning::NeedConnections` --- .../BDK+Extensions/CbfClient+Extensions.swift | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift index 6edf331d..e84cb950 100644 --- a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift +++ b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift @@ -29,7 +29,6 @@ extension CbfClient { func startBackgroundMonitoring() { Task { - var isConnected = false while true { if let log = try? await self.nextLog() { // Parse specific sync stage messages @@ -42,17 +41,6 @@ extension CbfClient { ) } } - - if log.contains("Need connections") && isConnected { - isConnected = false - await MainActor.run { - NotificationCenter.default.post( - name: NSNotification.Name("KyotoConnectionUpdate"), - object: nil, - userInfo: ["connected": false] - ) - } - } } } From a97544c8de7e7990a66d62b8ac08b525ca5fd3d8 Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 6 Aug 2025 16:04:33 -0500 Subject: [PATCH 11/40] set the progress to 20 percent after startup --- .../BDK+Extensions/CbfClient+Extensions.swift | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift index e84cb950..c805ad70 100644 --- a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift +++ b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift @@ -19,6 +19,14 @@ extension CbfClient { .build(wallet: wallet) components.node.run() + + // Send initial 20% progress after successful node startup + NotificationCenter.default.post( + name: NSNotification.Name("KyotoProgressUpdate"), + object: nil, + userInfo: ["progress": Float(0.2)] + ) + components.client.startBackgroundMonitoring() return (client: components.client, node: components.node) @@ -30,19 +38,7 @@ extension CbfClient { func startBackgroundMonitoring() { Task { while true { - if let log = try? await self.nextLog() { - // Parse specific sync stage messages - if log.contains("Attempting to load headers from the database") { - await MainActor.run { - NotificationCenter.default.post( - name: NSNotification.Name("KyotoProgressUpdate"), - object: nil, - userInfo: ["progress": Float(0.2)] - ) - } - } - } - + if let log = try? await self.nextLog() { } } } From 2e251d47a52578e22382a1c230b954c34542020f Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 6 Aug 2025 16:17:45 -0500 Subject: [PATCH 12/40] fix: progress calculation --- .../Extensions/BDK+Extensions/CbfClient+Extensions.swift | 2 +- BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift index c805ad70..5d0473a0 100644 --- a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift +++ b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift @@ -24,7 +24,7 @@ extension CbfClient { NotificationCenter.default.post( name: NSNotification.Name("KyotoProgressUpdate"), object: nil, - userInfo: ["progress": Float(0.2)] + userInfo: ["progress": Float(1)] ) components.client.startBackgroundMonitoring() diff --git a/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift b/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift index f064fd1a..b102e5fc 100644 --- a/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift +++ b/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift @@ -74,7 +74,7 @@ struct ActivityHomeHeaderView: View { Text( String( format: "%.0f%%", - progress * 100 + progress ) ) .contentTransition(.numericText()) @@ -138,7 +138,7 @@ struct ActivityHomeHeaderView: View { case .syncing: if isKyotoClient && progress > 0 { AnyView( - ProgressView(value: Double(progress * 100), total: 100) + ProgressView(value: Double(progress), total: 100) .foregroundStyle(.green) .frame(width: 20) .animation(.interactiveSpring, value: progress) From 9f7ebfa74d9f8d71f86c4e0e27b125e8a24ca246 Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 6 Aug 2025 16:27:30 -0500 Subject: [PATCH 13/40] ui: progressview (bar) increase width --- BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift b/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift index b102e5fc..f1dd3c63 100644 --- a/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift +++ b/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift @@ -140,7 +140,7 @@ struct ActivityHomeHeaderView: View { AnyView( ProgressView(value: Double(progress), total: 100) .foregroundStyle(.green) - .frame(width: 20) + .frame(width: 50) .animation(.interactiveSpring, value: progress) ) } else { From 364c28d5d61464c931507dea4a4193bdf1adea8f Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 7 Aug 2025 15:33:46 -0500 Subject: [PATCH 14/40] misc: comment --- .../Extensions/BDK+Extensions/CbfClient+Extensions.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift index 5d0473a0..83762aee 100644 --- a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift +++ b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift @@ -20,7 +20,7 @@ extension CbfClient { components.node.run() - // Send initial 20% progress after successful node startup + // Send initial 1% progress after successful node startup NotificationCenter.default.post( name: NSNotification.Name("KyotoProgressUpdate"), object: nil, From de50f9006beb3af610d48f2ea0c2871bf7369917 Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 7 Aug 2025 15:37:34 -0500 Subject: [PATCH 15/40] ui: hide fullscan button in settingsview (if kyoto) --- .../View/Settings/SettingsView.swift | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/BDKSwiftExampleWallet/View/Settings/SettingsView.swift b/BDKSwiftExampleWallet/View/Settings/SettingsView.swift index 167a445b..1f4649ee 100644 --- a/BDKSwiftExampleWallet/View/Settings/SettingsView.swift +++ b/BDKSwiftExampleWallet/View/Settings/SettingsView.swift @@ -18,6 +18,9 @@ struct SettingsView: View { var isSmallDevice: Bool { UIScreen.main.isPhoneSE } + private var isKyotoClient: Bool { + viewModel.bdkClient.getClientType() == .kyoto + } var body: some View { @@ -65,25 +68,27 @@ struct SettingsView: View { colorScheme == .light ? Color.gray.opacity(0.1) : Color.black.opacity(0.2) ) - Section(header: Text("Wallet")) { - Button { - Task { - await viewModel.fullScanWithProgress() + if !isKyotoClient { + Section(header: Text("Wallet")) { + Button { + Task { + await viewModel.fullScanWithProgress() + } + } label: { + Text("Full Scan") + } + .foregroundStyle(Color.bitcoinOrange) + if viewModel.walletSyncState == .syncing { + Text("\(viewModel.inspectedScripts)") + .contentTransition(.numericText()) + .foregroundStyle(.primary) + .animation(.easeInOut, value: viewModel.inspectedScripts) } - } label: { - Text("Full Scan") - } - .foregroundStyle(Color.bitcoinOrange) - if viewModel.walletSyncState == .syncing { - Text("\(viewModel.inspectedScripts)") - .contentTransition(.numericText()) - .foregroundStyle(.primary) - .animation(.easeInOut, value: viewModel.inspectedScripts) } + .listRowBackground( + colorScheme == .light ? Color.gray.opacity(0.1) : Color.black.opacity(0.2) + ) } - .listRowBackground( - colorScheme == .light ? Color.gray.opacity(0.1) : Color.black.opacity(0.2) - ) Section(header: Text("Danger Zone")) { Button { From 92c46709c7eb7abba5e62d8a5445f6e54d56f126 Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 7 Aug 2025 15:56:48 -0500 Subject: [PATCH 16/40] fix: auto-refresh wallet when new block height --- BDKSwiftExampleWallet/View Model/WalletViewModel.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift index 1f887a7a..f2dbde1e 100644 --- a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift @@ -117,6 +117,12 @@ class WalletViewModel { ) { [weak self] notification in if let height = notification.userInfo?["height"] as? UInt32 { self?.currentBlockHeight = height + // Auto-refresh wallet data when Kyoto receives new blocks + self?.getBalance() + self?.getTransactions() + Task { + await self?.getPrices() + } } } } From e82c6ccc760dff3748ea4b2238246fcc59c0dec0 Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 7 Aug 2025 16:04:27 -0500 Subject: [PATCH 17/40] fix: block height font size once synced --- BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift b/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift index f1dd3c63..b3b6bb33 100644 --- a/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift +++ b/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift @@ -94,6 +94,10 @@ struct ActivityHomeHeaderView: View { .fontWeight(.semibold) .contentTransition(.numericText()) .transition(.opacity) + .fontDesign(.monospaced) + .foregroundStyle(.secondary) + .font(.caption2) + .fontWeight(.thin) } } HStack { From c15e6e653998b311c942fd5cf1d9676fc6306b85 Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 7 Aug 2025 16:33:51 -0500 Subject: [PATCH 18/40] ui: always show latest height --- BDKSwiftExampleWallet/View/WalletView.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/BDKSwiftExampleWallet/View/WalletView.swift b/BDKSwiftExampleWallet/View/WalletView.swift index 629b3223..7c61a0d3 100644 --- a/BDKSwiftExampleWallet/View/WalletView.swift +++ b/BDKSwiftExampleWallet/View/WalletView.swift @@ -11,6 +11,7 @@ import SwiftUI struct WalletView: View { @AppStorage("balanceDisplayFormat") private var balanceFormat: BalanceDisplayFormat = .bitcoinSats + @AppStorage("KyotoLastBlockHeight") private var kyotoLastHeight: Int = 0 @Bindable var viewModel: WalletViewModel @Binding var sendNavigationPath: NavigationPath @State private var isFirstAppear = true @@ -127,6 +128,19 @@ struct WalletView: View { viewModel.getTransactions() await viewModel.getPrices() } + .onAppear { + // Seed height from AppStorage on first show to avoid displaying 0 when Kyoto is active + if viewModel.isKyotoClient, + viewModel.currentBlockHeight == 0, + kyotoLastHeight > 0 { + viewModel.currentBlockHeight = UInt32(kyotoLastHeight) + } + } + .onChange(of: viewModel.currentBlockHeight) { _, newValue in + if newValue > 0 { + kyotoLastHeight = Int(newValue) + } + } } .navigationDestination(isPresented: $showAllTransactions) { From b9d59e96afebe3b4d1f33dab6815cb94095de47d Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 7 Aug 2025 16:35:23 -0500 Subject: [PATCH 19/40] fix: kyoto could get into .notStarted state --- .../View Model/WalletViewModel.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift index f2dbde1e..56669096 100644 --- a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift @@ -97,6 +97,13 @@ class WalletViewModel { ) { [weak self] notification in if let progress = notification.userInfo?["progress"] as? Float { self?.updateKyotoProgress(progress) + + // Update sync state based on Kyoto progress + if progress >= 100 { + self?.walletSyncState = .synced + } else if progress > 0 { + self?.walletSyncState = .syncing + } } } @@ -107,6 +114,16 @@ class WalletViewModel { ) { [weak self] notification in if let connected = notification.userInfo?["connected"] as? Bool { self?.isKyotoConnected = connected + + // When Kyoto connects, update sync state if needed + if connected && self?.walletSyncState == .notStarted { + // Check current progress to determine state + if let progress = self?.progress, progress >= 100 { + self?.walletSyncState = .synced + } else { + self?.walletSyncState = .syncing + } + } } } From 20fb3127886ec619110a63b7336adcf99d956707 Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 7 Aug 2025 16:40:03 -0500 Subject: [PATCH 20/40] ui: dont show green checkmark for kyoto (already have connected green icon) --- .../View/Home/ActivityHomeHeaderView.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift b/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift index b3b6bb33..5e454f9c 100644 --- a/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift +++ b/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift @@ -134,10 +134,14 @@ struct ActivityHomeHeaderView: View { private func syncImageIndicator() -> some View { switch walletSyncState { case .synced: - AnyView( - Image(systemName: "checkmark.circle.fill") - .foregroundStyle(.green) - ) + if !isKyotoClient { + AnyView( + Image(systemName: "checkmark.circle.fill") + .foregroundStyle(.green) + ) + } else { + AnyView(EmptyView()) + } case .syncing: if isKyotoClient && progress > 0 { From 2fb7143a203662386b6aa235579f26824ceb80f5 Mon Sep 17 00:00:00 2001 From: Matthew Date: Fri, 8 Aug 2025 10:22:29 -0500 Subject: [PATCH 21/40] fix: restrict kyoto to signet --- .../Service/BDK Service/BDKService.swift | 24 +++++++++++++++++-- .../Settings/WalletRecoveryViewModel.swift | 3 ++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift index 2e2ce51f..76cd3540 100644 --- a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift +++ b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift @@ -100,6 +100,12 @@ private class BDKService { let storedClientType = try? keyClient.getClientType() self.clientType = storedClientType ?? .esplora + // Ensure Kyoto always uses Signet + if self.clientType == .kyoto && self.network != .signet { + self.network = .signet + try? keyClient.saveNetwork(Network.signet.description) + } + if self.clientType == .kyoto { self.blockchainURL = Constants.Config.Kyoto.getDefaultPeer(for: self.network) } else { @@ -118,6 +124,15 @@ private class BDKService { return } + // If Kyoto is selected force network to Signet and persist correction + if self.clientType == .kyoto && newNetwork != .signet { + self.network = .signet + try? keyClient.saveNetwork(Network.signet.description) + self.blockchainURL = Constants.Config.Kyoto.getDefaultPeer(for: .signet) + updateBlockchainClient() + return + } + self.network = newNetwork try? keyClient.saveNetwork(newNetwork.description) @@ -126,7 +141,7 @@ private class BDKService { let newURL = newNetwork.url updateBlockchainURL(newURL) } else if self.clientType == .kyoto { - // For Kyoto, update to the correct peer for the new network + // For Kyoto update to the correct peer for the new network let newPeer = Constants.Config.Kyoto.getDefaultPeer(for: newNetwork) self.blockchainURL = newPeer updateBlockchainClient() @@ -666,7 +681,12 @@ extension BDKService { // Update URL to match the new client type if newType == .kyoto { - self.blockchainURL = Constants.Config.Kyoto.getDefaultPeer(for: self.network) + // Force Signet network for Kyoto and persist the corrected network + if self.network != .signet { + self.network = .signet + try? keyClient.saveNetwork(Network.signet.description) + } + self.blockchainURL = Constants.Config.Kyoto.getDefaultPeer(for: .signet) } else if newType == .esplora { // Keep existing URL if it's valid for this network, otherwise use default let defaultEsploraURL = self.network.url diff --git a/BDKSwiftExampleWallet/View Model/Settings/WalletRecoveryViewModel.swift b/BDKSwiftExampleWallet/View Model/Settings/WalletRecoveryViewModel.swift index 0d613d2b..6a799a0f 100644 --- a/BDKSwiftExampleWallet/View Model/Settings/WalletRecoveryViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/Settings/WalletRecoveryViewModel.swift @@ -34,7 +34,8 @@ class WalletRecoveryViewModel { func getNetwork() -> Network { let savedNetwork = bdkClient.getNetwork() - return savedNetwork + let clientType = bdkClient.getClientType() + return clientType == .kyoto ? .signet : savedNetwork } func getBackupInfo(network: Network) { From 303aeb4ef841d4105d98dcd0a0aad0cded44b7e4 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 11 Aug 2025 10:03:13 -0500 Subject: [PATCH 22/40] ui: load previous transaction state when app restart --- .../View Model/Activity/ActivityListViewModel.swift | 13 +++++++++++++ BDKSwiftExampleWallet/View/WalletView.swift | 8 +++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/BDKSwiftExampleWallet/View Model/Activity/ActivityListViewModel.swift b/BDKSwiftExampleWallet/View Model/Activity/ActivityListViewModel.swift index 5519e3d3..004eee4c 100644 --- a/BDKSwiftExampleWallet/View Model/Activity/ActivityListViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/Activity/ActivityListViewModel.swift @@ -46,6 +46,19 @@ class ActivityListViewModel { self.bdkClient = bdkClient self.transactions = transactions self.walletSyncState = walletSyncState + + // Preload cached data synchronously so UI has content before first render + // transactions + listUnspent items are available from the persisted wallet db + if self.transactions.isEmpty { + if let cached = try? bdkClient.transactions() { + self.transactions = cached + } + } + if self.localOutputs.isEmpty { + if let cachedUtxos = try? bdkClient.listUnspent() { + self.localOutputs = cachedUtxos + } + } } func getTransactions() { diff --git a/BDKSwiftExampleWallet/View/WalletView.swift b/BDKSwiftExampleWallet/View/WalletView.swift index 7c61a0d3..bd0c0d19 100644 --- a/BDKSwiftExampleWallet/View/WalletView.swift +++ b/BDKSwiftExampleWallet/View/WalletView.swift @@ -110,6 +110,11 @@ struct WalletView: View { ), perform: { _ in Task { + // Show cached state first + viewModel.getBalance() + viewModel.getTransactions() + + // Then sync and refresh await viewModel.syncOrFullScan() viewModel.getBalance() viewModel.getTransactions() @@ -119,13 +124,14 @@ struct WalletView: View { ) .task { viewModel.getBalance() + viewModel.getTransactions() if isFirstAppear || newTransactionSent { await viewModel.syncOrFullScan() isFirstAppear = false newTransactionSent = false viewModel.getBalance() + viewModel.getTransactions() } - viewModel.getTransactions() await viewModel.getPrices() } .onAppear { From 2877b4b5d1cb7e3f3e433c970f5273938a6fbf37 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 11 Aug 2025 10:22:47 -0500 Subject: [PATCH 23/40] fix: progress percent in walletviewmodel --- BDKSwiftExampleWallet/View Model/WalletViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift index 56669096..05ff4428 100644 --- a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift @@ -62,7 +62,7 @@ class WalletViewModel { { [weak self] progress in DispatchQueue.main.async { self?.progress = progress - let progressPercent = UInt64(progress * 100) + let progressPercent = UInt64(progress) self?.inspectedScripts = progressPercent self?.totalScripts = 100 } From b2be622c30382f3adeed8ab6f53044301057bbfc Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 11 Aug 2025 10:41:06 -0500 Subject: [PATCH 24/40] fix: client progress values are different --- .../Resources/Localizable.xcstrings | 6 +++ .../View/Home/ActivityHomeHeaderView.swift | 42 +++++++++---------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/BDKSwiftExampleWallet/Resources/Localizable.xcstrings b/BDKSwiftExampleWallet/Resources/Localizable.xcstrings index 98a6f145..d601fe7b 100644 --- a/BDKSwiftExampleWallet/Resources/Localizable.xcstrings +++ b/BDKSwiftExampleWallet/Resources/Localizable.xcstrings @@ -146,6 +146,9 @@ } } } + }, + "%" : { + }, "%@ • %@" : { "localizations" : { @@ -210,6 +213,9 @@ } } } + }, + "%lld" : { + }, "%lld Output%@" : { "localizations" : { diff --git a/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift b/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift index 5e454f9c..411c40d5 100644 --- a/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift +++ b/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift @@ -39,8 +39,8 @@ struct ActivityHomeHeaderView: View { .animation(.easeInOut, value: inspectedScripts) } else if walletSyncState == .syncing { HStack { - if progress < 1.0 { - if isKyotoClient { + if isKyotoClient { + if progress < 100.0 { // Kyoto progress is percent if currentBlockHeight > 0 { Text("Block \(currentBlockHeight)") .padding(.trailing, -5.0) @@ -54,30 +54,28 @@ struct ActivityHomeHeaderView: View { .contentTransition(.numericText()) .transition(.opacity) } - } else { - Text("\(inspectedScripts)") - .padding(.trailing, -5.0) - .fontWeight(.semibold) - .contentTransition(.numericText()) - .transition(.opacity) - - Text("/") - .padding(.trailing, -5.0) - .transition(.opacity) - Text("\(totalScripts)") - .contentTransition(.numericText()) - .transition(.opacity) } + } else if progress < 1.0 { // Esplora progress is fraction + Text("\(inspectedScripts)") + .padding(.trailing, -5.0) + .fontWeight(.semibold) + .contentTransition(.numericText()) + .transition(.opacity) + + Text("/") + .padding(.trailing, -5.0) + .transition(.opacity) + Text("\(totalScripts)") + .contentTransition(.numericText()) + .transition(.opacity) } if !isKyotoClient || (isKyotoClient && progress > 0) { - Text( - String( - format: "%.0f%%", - progress - ) - ) - .contentTransition(.numericText()) + HStack(spacing: 0) { + Text("\(Int(progress))") + .contentTransition(.numericText()) + Text("%") + } .transition(.opacity) } } From 072c51304ce35ca55b30c1951043a8fa42159faf Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 11 Aug 2025 11:12:29 -0500 Subject: [PATCH 25/40] fix: network switch because of kyoto forced network signet --- .../Service/BDK Service/BDKService.swift | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift index 76cd3540..825c51f8 100644 --- a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift +++ b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift @@ -518,20 +518,14 @@ private class BDKService { } func deleteWallet() throws { - let savedURL = try? keyClient.getEsploraURL() - let savedNetwork = try? keyClient.getNetwork() - if let bundleID = Bundle.main.bundleIdentifier { UserDefaults.standard.removePersistentDomain(forName: bundleID) } try self.keyClient.deleteBackupInfo() try Persister.deleteConnection() - if let savedURL = savedURL { - try keyClient.saveEsploraURL(savedURL) - } - if let savedNetwork = savedNetwork { - try keyClient.saveNetwork(savedNetwork) - } + // Clear persisted network and esplora URL to avoid cross-network carryover + try? keyClient.deleteNetwork() + try? keyClient.deleteEsplora() needsFullScan = true } From e8c9eb60ce3c0624a1b184c7f047b17375bd9eb6 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 11 Aug 2025 11:29:16 -0500 Subject: [PATCH 26/40] fix: regression from 2fb7143a203662386b6aa235579f26824ceb80f5 --- BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift index 825c51f8..56579ce9 100644 --- a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift +++ b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift @@ -100,10 +100,10 @@ private class BDKService { let storedClientType = try? keyClient.getClientType() self.clientType = storedClientType ?? .esplora - // Ensure Kyoto always uses Signet + // If starting in Kyoto, constrain in-memory network to Signet, but do not persist here. + // Persistence should happen only when the user confirms Kyoto via updateClientType/updateNetwork. if self.clientType == .kyoto && self.network != .signet { self.network = .signet - try? keyClient.saveNetwork(Network.signet.description) } if self.clientType == .kyoto { From 6673a1a640f6d53d4df99d4eb492fe37c30e8696 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 11 Aug 2025 12:17:39 -0500 Subject: [PATCH 27/40] fix: make sure kyoto tasks killed when switching networks --- .../BDK+Extensions/CbfClient+Extensions.swift | 73 ++++++++----------- .../Service/BDK Service/BDKService.swift | 33 +++------ .../View Model/WalletViewModel.swift | 20 +++-- 3 files changed, 53 insertions(+), 73 deletions(-) diff --git a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift index 83762aee..189312a2 100644 --- a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift +++ b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift @@ -9,6 +9,10 @@ import BitcoinDevKit import Foundation extension CbfClient { + // Track one monitoring task per client for clean cancellation + private static var monitoringTasks: [ObjectIdentifier: Task] = [:] + private static let monitoringTasksQueue = DispatchQueue(label: "cbf.monitoring.tasks") + static func createComponents(wallet: Wallet) -> (client: CbfClient, node: CbfNode) { do { let components = try CbfBuilder() @@ -20,13 +24,6 @@ extension CbfClient { components.node.run() - // Send initial 1% progress after successful node startup - NotificationCenter.default.post( - name: NSNotification.Name("KyotoProgressUpdate"), - object: nil, - userInfo: ["progress": Float(1)] - ) - components.client.startBackgroundMonitoring() return (client: components.client, node: components.node) @@ -36,16 +33,14 @@ extension CbfClient { } func startBackgroundMonitoring() { - Task { - while true { - if let log = try? await self.nextLog() { } - } - } + let id = ObjectIdentifier(self) - Task { + let task = Task { [self] in var hasEstablishedConnection = false while true { - if let info = try? await self.nextInfo() { + if Task.isCancelled { break } + do { + let info = try await self.nextInfo() switch info { case let .progress(progress): await MainActor.run { @@ -62,7 +57,6 @@ extension CbfClient { object: nil, userInfo: ["height": height] ) - if !hasEstablishedConnection { hasEstablishedConnection = true NotificationCenter.default.post( @@ -72,16 +66,7 @@ extension CbfClient { ) } } - case .connectionsMet: - await MainActor.run { - hasEstablishedConnection = true - NotificationCenter.default.post( - name: NSNotification.Name("KyotoConnectionUpdate"), - object: nil, - userInfo: ["connected": true] - ) - } - case .successfulHandshake: + case .connectionsMet, .successfulHandshake: await MainActor.run { if !hasEstablishedConnection { hasEstablishedConnection = true @@ -95,27 +80,31 @@ extension CbfClient { default: break } + } catch is CancellationError { + break + } catch { + // ignore } } } - Task { - while true { - if let warning = try? await self.nextWarning() { - switch warning { - case .needConnections: - await MainActor.run { - NotificationCenter.default.post( - name: NSNotification.Name("KyotoConnectionUpdate"), - object: nil, - userInfo: ["connected": false] - ) - } - default: - break - } - } - } + Self.monitoringTasksQueue.sync { + Self.monitoringTasks[id] = task + } + } + + func stopBackgroundMonitoring() { + let id = ObjectIdentifier(self) + Self.monitoringTasksQueue.sync { + guard let task = Self.monitoringTasks.removeValue(forKey: id) else { return } + task.cancel() + } + } + + static func cancelAllMonitoring() { + Self.monitoringTasksQueue.sync { + for (_, task) in Self.monitoringTasks { task.cancel() } + Self.monitoringTasks.removeAll() } } } diff --git a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift index 56579ce9..4a48178c 100644 --- a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift +++ b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift @@ -100,11 +100,7 @@ private class BDKService { let storedClientType = try? keyClient.getClientType() self.clientType = storedClientType ?? .esplora - // If starting in Kyoto, constrain in-memory network to Signet, but do not persist here. - // Persistence should happen only when the user confirms Kyoto via updateClientType/updateNetwork. - if self.clientType == .kyoto && self.network != .signet { - self.network = .signet - } + // No init-time coercion; backend selection handles constraints if self.clientType == .kyoto { self.blockchainURL = Constants.Config.Kyoto.getDefaultPeer(for: self.network) @@ -124,15 +120,6 @@ private class BDKService { return } - // If Kyoto is selected force network to Signet and persist correction - if self.clientType == .kyoto && newNetwork != .signet { - self.network = .signet - try? keyClient.saveNetwork(Network.signet.description) - self.blockchainURL = Constants.Config.Kyoto.getDefaultPeer(for: .signet) - updateBlockchainClient() - return - } - self.network = newNetwork try? keyClient.saveNetwork(newNetwork.description) @@ -140,31 +127,28 @@ private class BDKService { if self.clientType == .esplora { let newURL = newNetwork.url updateBlockchainURL(newURL) - } else if self.clientType == .kyoto { - // For Kyoto update to the correct peer for the new network - let newPeer = Constants.Config.Kyoto.getDefaultPeer(for: newNetwork) - self.blockchainURL = newPeer - updateBlockchainClient() } } } func updateBlockchainURL(_ newURL: String) { - if newURL != self.blockchainURL { - self.blockchainURL = newURL - try? keyClient.saveEsploraURL(newURL) - updateBlockchainClient() - } + if newURL == self.blockchainURL { return } + self.blockchainURL = newURL + try? keyClient.saveEsploraURL(newURL) + updateBlockchainClient() } internal func updateBlockchainClient() { do { switch clientType { case .esplora: + // Cancel any Kyoto background tasks when switching to Esplora + CbfClient.cancelAllMonitoring() self.blockchainClient = .esplora(url: self.blockchainURL) case .kyoto: if self.network != .signet { self.clientType = .esplora + CbfClient.cancelAllMonitoring() self.blockchainClient = .esplora(url: self.blockchainURL) } else { let peer = @@ -178,6 +162,7 @@ private class BDKService { } } catch { self.clientType = .esplora + CbfClient.cancelAllMonitoring() self.blockchainClient = .esplora(url: self.blockchainURL) } } diff --git a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift index 05ff4428..84e85fee 100644 --- a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift @@ -95,14 +95,17 @@ class WalletViewModel { object: nil, queue: .main ) { [weak self] notification in + guard let self else { return } + // Ignore Kyoto updates unless client type is Kyoto + if self.bdkClient.getClientType() != .kyoto { return } if let progress = notification.userInfo?["progress"] as? Float { - self?.updateKyotoProgress(progress) + self.updateKyotoProgress(progress) // Update sync state based on Kyoto progress if progress >= 100 { - self?.walletSyncState = .synced + self.walletSyncState = .synced } else if progress > 0 { - self?.walletSyncState = .syncing + self.walletSyncState = .syncing } } } @@ -132,13 +135,16 @@ class WalletViewModel { object: nil, queue: .main ) { [weak self] notification in + 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 + self.currentBlockHeight = height // Auto-refresh wallet data when Kyoto receives new blocks - self?.getBalance() - self?.getTransactions() + self.getBalance() + self.getTransactions() Task { - await self?.getPrices() + await self.getPrices() } } } From 6e7c63150d565fd2778b23548c32812f65fd29be Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 11 Aug 2025 12:28:53 -0500 Subject: [PATCH 28/40] fix: esplora percent progress --- .../View Model/WalletViewModel.swift | 12 +++++++++++- .../View/Home/ActivityHomeHeaderView.swift | 5 ++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift index 84e85fee..ed84bb24 100644 --- a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift @@ -51,9 +51,16 @@ class WalletViewModel { private var updateProgress: @Sendable (UInt64, UInt64) -> Void { { [weak self] inspected, total in DispatchQueue.main.async { + // When using Kyoto, progress is provided separately as percent + if self?.isKyotoClient == true { return } self?.totalScripts = total self?.inspectedScripts = inspected - self?.progress = total > 0 ? Float(inspected) / Float(total) : 0 + let fraction = total > 0 ? Float(inspected) / Float(total) : 0 + self?.progress = fraction + #if DEBUG + let percent = Int((fraction * 100).rounded()) + print("[Esplora][VM] inspected=\(inspected)/\(total) fraction=\(String(format: "%.4f", fraction)) percent=\(percent)%") + #endif } } } @@ -65,6 +72,9 @@ class WalletViewModel { let progressPercent = UInt64(progress) self?.inspectedScripts = progressPercent self?.totalScripts = 100 + #if DEBUG + print("[Kyoto][VM] percent=\(Int(progress))%") + #endif } } } diff --git a/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift b/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift index 411c40d5..aeb6836c 100644 --- a/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift +++ b/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift @@ -71,8 +71,11 @@ struct ActivityHomeHeaderView: View { } if !isKyotoClient || (isKyotoClient && progress > 0) { + let percent: Int = isKyotoClient + ? Int(progress.rounded()) + : Int((progress * 100).rounded()) HStack(spacing: 0) { - Text("\(Int(progress))") + Text("\(percent)") .contentTransition(.numericText()) Text("%") } From 460da009beba986e175d00950ee5af339d3d3622 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 11 Aug 2025 12:48:04 -0500 Subject: [PATCH 29/40] chore: temporary logging for kyoto due to not getting info from peer today --- .../BDK+Extensions/CbfClient+Extensions.swift | 48 ++++++++++++++++++- .../Service/BDK Service/BDKService.swift | 21 +++++++- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift index 189312a2..f1db2795 100644 --- a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift +++ b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift @@ -9,8 +9,10 @@ import BitcoinDevKit import Foundation extension CbfClient { - // Track one monitoring task per client for clean cancellation + // Track monitoring tasks per client for clean cancellation private static var monitoringTasks: [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) { @@ -23,8 +25,14 @@ extension CbfClient { .build(wallet: wallet) components.node.run() + #if DEBUG + print("[Kyoto] node started; peers=\(Constants.Networks.Signet.Regular.kyotoPeers.count)") + #endif components.client.startBackgroundMonitoring() + #if DEBUG + print("[Kyoto] background monitoring started") + #endif return (client: components.client, node: components.node) } catch { @@ -41,8 +49,12 @@ extension CbfClient { if Task.isCancelled { break } do { let info = try await self.nextInfo() + CbfClient.monitoringTasksQueue.sync { Self.lastInfoAt[id] = Date() } switch info { case let .progress(progress): + #if DEBUG + print("[Kyoto] progress: \(progress)") + #endif await MainActor.run { NotificationCenter.default.post( name: NSNotification.Name("KyotoProgressUpdate"), @@ -51,6 +63,9 @@ extension CbfClient { ) } case let .newChainHeight(height): + #if DEBUG + print("[Kyoto] newChainHeight: \(height)") + #endif await MainActor.run { NotificationCenter.default.post( name: NSNotification.Name("KyotoChainHeightUpdate"), @@ -67,6 +82,9 @@ extension CbfClient { } } case .connectionsMet, .successfulHandshake: + #if DEBUG + print("[Kyoto] connections established") + #endif await MainActor.run { if !hasEstablishedConnection { hasEstablishedConnection = true @@ -90,6 +108,29 @@ extension CbfClient { Self.monitoringTasksQueue.sync { Self.monitoringTasks[id] = task + Self.lastInfoAt[id] = Date() + } + + // Heartbeat task to signal idleness while awaiting Info events + let heartbeat = Task { + while true { + if Task.isCancelled { break } + try? await Task.sleep(nanoseconds: 5_000_000_000) + if Task.isCancelled { break } + var idleFor: TimeInterval = 0 + CbfClient.monitoringTasksQueue.sync { + if let last = Self.lastInfoAt[id] { idleFor = Date().timeIntervalSince(last) } + } + #if DEBUG + if idleFor >= 5 { + print("[Kyoto] idle: waiting for info… \(Int(idleFor))s") + } + #endif + } + } + + Self.monitoringTasksQueue.sync { + Self.heartbeatTasks[id] = heartbeat } } @@ -98,13 +139,18 @@ extension CbfClient { Self.monitoringTasksQueue.sync { guard let task = Self.monitoringTasks.removeValue(forKey: id) else { return } task.cancel() + if let hb = Self.heartbeatTasks.removeValue(forKey: id) { hb.cancel() } + Self.lastInfoAt.removeValue(forKey: id) } } static func cancelAllMonitoring() { Self.monitoringTasksQueue.sync { for (_, task) in Self.monitoringTasks { task.cancel() } + for (_, hb) in Self.heartbeatTasks { hb.cancel() } Self.monitoringTasks.removeAll() + Self.heartbeatTasks.removeAll() + Self.lastInfoAt.removeAll() } } } diff --git a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift index 4a48178c..b7d797b0 100644 --- a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift +++ b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift @@ -63,11 +63,25 @@ extension BlockchainClient { return Self( sync: { request, _ in let components = try getOrCreateComponents() - return try await components.client.update() + #if DEBUG + print("[Kyoto][BDKService] calling update() (sync)") + #endif + let upd = try await components.client.update() + #if DEBUG + print("[Kyoto][BDKService] update() returned (sync)") + #endif + return upd }, fullScan: { request, stopGap, _ in let components = try getOrCreateComponents() - return try await components.client.update() + #if DEBUG + print("[Kyoto][BDKService] calling update() (fullScan)") + #endif + let upd = try await components.client.update() + #if DEBUG + print("[Kyoto][BDKService] update() returned (fullScan)") + #endif + return upd }, broadcast: { tx in let components = try getOrCreateComponents() @@ -155,6 +169,9 @@ private class BDKService { self.blockchainURL.isEmpty ? Constants.Config.Kyoto.getDefaultPeer(for: self.network) : self.blockchainURL + #if DEBUG + print("[BDKService] selecting Kyoto peer=\(peer)") + #endif self.blockchainClient = .kyoto(peer: peer) } case .electrum: From 9062dc4cf79f746ac05ed3efb50def19578f230e Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 11 Aug 2025 12:52:34 -0500 Subject: [PATCH 30/40] chore: more temp logging because not getting peer info today --- .../BDK+Extensions/CbfClient+Extensions.swift | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift index f1db2795..34cec9ae 100644 --- a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift +++ b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift @@ -11,6 +11,7 @@ import Foundation 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 heartbeatTasks: [ObjectIdentifier: Task] = [:] private static var lastInfoAt: [ObjectIdentifier: Date] = [:] private static let monitoringTasksQueue = DispatchQueue(label: "cbf.monitoring.tasks") @@ -132,6 +133,36 @@ extension CbfClient { Self.monitoringTasksQueue.sync { Self.heartbeatTasks[id] = heartbeat } + + // Minimal warnings listener for visibility while syncing + let warnings = Task { [self] in + while true { + if Task.isCancelled { break } + do { + let warning = try await self.nextWarning() + #if DEBUG + print("[Kyoto][warning] \(String(describing: warning))") + #endif + if case .needConnections = warning { + await MainActor.run { + NotificationCenter.default.post( + name: NSNotification.Name("KyotoConnectionUpdate"), + object: nil, + userInfo: ["connected": false] + ) + } + } + } catch is CancellationError { + break + } catch { + // ignore + } + } + } + + Self.monitoringTasksQueue.sync { + Self.warningTasks[id] = warnings + } } func stopBackgroundMonitoring() { @@ -140,6 +171,7 @@ extension CbfClient { guard let task = Self.monitoringTasks.removeValue(forKey: id) else { return } task.cancel() if let hb = Self.heartbeatTasks.removeValue(forKey: id) { hb.cancel() } + if let wt = Self.warningTasks.removeValue(forKey: id) { wt.cancel() } Self.lastInfoAt.removeValue(forKey: id) } } @@ -147,8 +179,10 @@ extension CbfClient { static func cancelAllMonitoring() { Self.monitoringTasksQueue.sync { for (_, task) in Self.monitoringTasks { task.cancel() } + for (_, wt) in Self.warningTasks { wt.cancel() } for (_, hb) in Self.heartbeatTasks { hb.cancel() } Self.monitoringTasks.removeAll() + Self.warningTasks.removeAll() Self.heartbeatTasks.removeAll() Self.lastInfoAt.removeAll() } From 19be0795a73d7d768b437835acc2c0c65bdbced1 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 11 Aug 2025 12:59:04 -0500 Subject: [PATCH 31/40] chore: temp log making sure data dir is ok for peer info --- .../BDK+Extensions/CbfClient+Extensions.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift index 34cec9ae..8a6d34c3 100644 --- a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift +++ b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift @@ -18,6 +18,22 @@ extension CbfClient { static func createComponents(wallet: Wallet) -> (client: CbfClient, node: CbfNode) { do { + #if DEBUG + let dataDirPath = Constants.Config.Kyoto.dbPath + print("[Kyoto] dataDir: \(dataDirPath)") + do { + let testFile = (dataDirPath as NSString).appendingPathComponent(".write_test") + try Data("ok".utf8).write(to: URL(fileURLWithPath: testFile)) + try? FileManager.default.removeItem(atPath: testFile) + print("[Kyoto] dataDir writable: true") + } catch { + print("[Kyoto] dataDir writable: false error=\(error)") + } + let peers = Constants.Networks.Signet.Regular.kyotoPeers + print("[Kyoto] peers count: \(peers.count)") + for peer in peers { print("[Kyoto] peer: \(peer)") } + #endif + let components = try CbfBuilder() .logLevel(logLevel: .debug) .scanType(scanType: .sync) From 466fa2614aafe4595eddb2790a9e0cb51e23c1f0 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 11 Aug 2025 13:04:09 -0500 Subject: [PATCH 32/40] chore: temp logging nextlog --- .../BDK+Extensions/CbfClient+Extensions.swift | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift index 8a6d34c3..023530e5 100644 --- a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift +++ b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift @@ -12,6 +12,7 @@ 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") @@ -179,6 +180,30 @@ extension CbfClient { Self.monitoringTasksQueue.sync { Self.warningTasks[id] = warnings } + + // Log listener for detailed debugging + let logs = Task { [self] in + while true { + if Task.isCancelled { break } + do { + #if DEBUG + print("[Kyoto] calling nextLog()") + #endif + let log = try await self.nextLog() + #if DEBUG + print("[Kyoto] nextLog() returned: \(log)") + #endif + } catch is CancellationError { + break + } catch { + // ignore + } + } + } + + Self.monitoringTasksQueue.sync { + Self.logTasks[id] = logs + } } func stopBackgroundMonitoring() { @@ -188,6 +213,7 @@ 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) } } @@ -196,9 +222,11 @@ 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() } From 862f3f1ab7e08cad4c95287d7267547aee8f0e53 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 11 Aug 2025 13:06:51 -0500 Subject: [PATCH 33/40] misc: remove onion --- BDKSwiftExampleWallet/Utilities/Constants.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/BDKSwiftExampleWallet/Utilities/Constants.swift b/BDKSwiftExampleWallet/Utilities/Constants.swift index 520d942a..fba90c5f 100644 --- a/BDKSwiftExampleWallet/Utilities/Constants.swift +++ b/BDKSwiftExampleWallet/Utilities/Constants.swift @@ -72,7 +72,6 @@ struct Constants { static let kyotoPeerStrings = [ "seed.signet.bitcoin.sprovoost.nl:38333", "signet-seed.achownodes.xyz:38333", - "vrajjeirttkmnt32wpy3cowmnwr13fkla7hpxc4okr3ysd3kqtzmqd.onion:38333", ] static let kyotoPeers = [ From c1977a04aeb60d22538780910064637a8504a90d Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 11 Aug 2025 13:47:54 -0500 Subject: [PATCH 34/40] ui: show connected if progress or height --- BDKSwiftExampleWallet/View Model/WalletViewModel.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift index ed84bb24..248f168f 100644 --- a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift @@ -110,6 +110,11 @@ class WalletViewModel { if self.bdkClient.getClientType() != .kyoto { return } if let progress = notification.userInfo?["progress"] as? Float { self.updateKyotoProgress(progress) + // 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 { + self.isKyotoConnected = true + } // Update sync state based on Kyoto progress if progress >= 100 { @@ -150,6 +155,8 @@ class WalletViewModel { if self.bdkClient.getClientType() != .kyoto { return } if let height = notification.userInfo?["height"] as? UInt32 { self.currentBlockHeight = height + // Receiving chain height implies we have peer connectivity + self.isKyotoConnected = true // Auto-refresh wallet data when Kyoto receives new blocks self.getBalance() self.getTransactions() From ada2f0af93fa2876dbc0d04de6c33faef5175714 Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 14 Aug 2025 11:05:14 -0500 Subject: [PATCH 35/40] fix: infrequent but persistent issue with ui state --- BDKSwiftExampleWallet/View Model/WalletViewModel.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift index 248f168f..a59937ef 100644 --- a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift @@ -250,6 +250,7 @@ class WalletViewModel { } func syncOrFullScan() async { + self.walletSyncState = .syncing if bdkClient.needsFullScan() { await fullScanWithProgress() bdkClient.setNeedsFullScan(false) From 997638907504ad135d5419fddac24ffc4274cfbc Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 14 Aug 2025 11:07:20 -0500 Subject: [PATCH 36/40] fix: infrequent but persistent issue with ui state (part 2) --- .../View Model/Activity/ActivityListViewModel.swift | 1 + .../View Model/Settings/SettingsViewModel.swift | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/BDKSwiftExampleWallet/View Model/Activity/ActivityListViewModel.swift b/BDKSwiftExampleWallet/View Model/Activity/ActivityListViewModel.swift index 004eee4c..6a6c7195 100644 --- a/BDKSwiftExampleWallet/View Model/Activity/ActivityListViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/Activity/ActivityListViewModel.swift @@ -111,6 +111,7 @@ class ActivityListViewModel { } func syncOrFullScan() async { + self.walletSyncState = .syncing await startSyncWithProgress() } } diff --git a/BDKSwiftExampleWallet/View Model/Settings/SettingsViewModel.swift b/BDKSwiftExampleWallet/View Model/Settings/SettingsViewModel.swift index fde0f7f4..98615666 100644 --- a/BDKSwiftExampleWallet/View Model/Settings/SettingsViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/Settings/SettingsViewModel.swift @@ -62,9 +62,7 @@ class SettingsViewModel: ObservableObject { } func fullScanWithProgress() async { - DispatchQueue.main.async { - self.walletSyncState = .syncing - } + self.walletSyncState = .syncing do { let inspector = WalletFullScanScriptInspector(updateProgress: updateProgressFullScan) try await bdkClient.fullScanWithInspector(inspector) From b85d9de8b0b5188e75f0f171658f31ab8cf66324 Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 14 Aug 2025 11:37:56 -0500 Subject: [PATCH 37/40] fix: block height not showing on sync sometimes for kyoto --- BDKSwiftExampleWallet/View Model/WalletViewModel.swift | 2 ++ BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift index a59937ef..cda8ca1f 100644 --- a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift @@ -157,6 +157,8 @@ class WalletViewModel { self.currentBlockHeight = height // Receiving chain height implies we have peer connectivity self.isKyotoConnected = true + // Ensure UI reflects syncing as soon as we see chain activity + if self.walletSyncState == .notStarted { self.walletSyncState = .syncing } // Auto-refresh wallet data when Kyoto receives new blocks self.getBalance() self.getTransactions() diff --git a/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift b/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift index aeb6836c..42f5b589 100644 --- a/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift +++ b/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift @@ -26,7 +26,7 @@ struct ActivityHomeHeaderView: View { Spacer() HStack { - if needsFullScan { + if needsFullScan && !isKyotoClient { Text("\(inspectedScripts)") .padding(.trailing, -5.0) .fontWeight(.semibold) From e31d484d1613af9321dc6f7cbb822b50c95fbcb6 Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 14 Aug 2025 14:45:26 -0500 Subject: [PATCH 38/40] ui: fine tune connected state by adding gray state --- .../View/Home/ActivityHomeHeaderView.swift | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift b/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift index 42f5b589..be0b1d68 100644 --- a/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift +++ b/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift @@ -176,15 +176,23 @@ struct ActivityHomeHeaderView: View { @ViewBuilder private func networkConnectionIndicator() -> some View { - if isKyotoConnected { + // Tri-state indicator for Kyoto peer connectivity + // - Green: actively connected OR showing sync activity + // - Gray (secondary): synced but not currently connected + // - Red: not synced, no activity, and not connected + let isFullySynced = walletSyncState == .synced + let hasSyncActivity = (progress > 0) || (currentBlockHeight > 0) + + if isFullySynced { AnyView( Image(systemName: "network") - .foregroundStyle(.green) + .foregroundStyle(isKyotoConnected ? .green : .secondary) ) } else { + let ok = isKyotoConnected || hasSyncActivity AnyView( - Image(systemName: "network.slash") - .foregroundStyle(.red) + Image(systemName: ok ? "network" : "network.slash") + .foregroundStyle(ok ? .green : .red) ) } } From 286c39b9e8539c9e3b10e5a577df8f81758e7d4f Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 14 Aug 2025 14:51:09 -0500 Subject: [PATCH 39/40] misc: remove print --- .../BDK+Extensions/CbfClient+Extensions.swift | 44 ------------------- .../Service/BDK Service/BDKService.swift | 15 ------- .../View Model/WalletViewModel.swift | 7 --- 3 files changed, 66 deletions(-) diff --git a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift index 023530e5..4f31de25 100644 --- a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift +++ b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift @@ -19,21 +19,6 @@ extension CbfClient { static func createComponents(wallet: Wallet) -> (client: CbfClient, node: CbfNode) { do { - #if DEBUG - let dataDirPath = Constants.Config.Kyoto.dbPath - print("[Kyoto] dataDir: \(dataDirPath)") - do { - let testFile = (dataDirPath as NSString).appendingPathComponent(".write_test") - try Data("ok".utf8).write(to: URL(fileURLWithPath: testFile)) - try? FileManager.default.removeItem(atPath: testFile) - print("[Kyoto] dataDir writable: true") - } catch { - print("[Kyoto] dataDir writable: false error=\(error)") - } - let peers = Constants.Networks.Signet.Regular.kyotoPeers - print("[Kyoto] peers count: \(peers.count)") - for peer in peers { print("[Kyoto] peer: \(peer)") } - #endif let components = try CbfBuilder() .logLevel(logLevel: .debug) @@ -43,14 +28,8 @@ extension CbfClient { .build(wallet: wallet) components.node.run() - #if DEBUG - print("[Kyoto] node started; peers=\(Constants.Networks.Signet.Regular.kyotoPeers.count)") - #endif components.client.startBackgroundMonitoring() - #if DEBUG - print("[Kyoto] background monitoring started") - #endif return (client: components.client, node: components.node) } catch { @@ -70,9 +49,6 @@ extension CbfClient { CbfClient.monitoringTasksQueue.sync { Self.lastInfoAt[id] = Date() } switch info { case let .progress(progress): - #if DEBUG - print("[Kyoto] progress: \(progress)") - #endif await MainActor.run { NotificationCenter.default.post( name: NSNotification.Name("KyotoProgressUpdate"), @@ -81,9 +57,6 @@ extension CbfClient { ) } case let .newChainHeight(height): - #if DEBUG - print("[Kyoto] newChainHeight: \(height)") - #endif await MainActor.run { NotificationCenter.default.post( name: NSNotification.Name("KyotoChainHeightUpdate"), @@ -100,9 +73,6 @@ extension CbfClient { } } case .connectionsMet, .successfulHandshake: - #if DEBUG - print("[Kyoto] connections established") - #endif await MainActor.run { if !hasEstablishedConnection { hasEstablishedConnection = true @@ -139,11 +109,6 @@ extension CbfClient { CbfClient.monitoringTasksQueue.sync { if let last = Self.lastInfoAt[id] { idleFor = Date().timeIntervalSince(last) } } - #if DEBUG - if idleFor >= 5 { - print("[Kyoto] idle: waiting for info… \(Int(idleFor))s") - } - #endif } } @@ -157,9 +122,6 @@ extension CbfClient { if Task.isCancelled { break } do { let warning = try await self.nextWarning() - #if DEBUG - print("[Kyoto][warning] \(String(describing: warning))") - #endif if case .needConnections = warning { await MainActor.run { NotificationCenter.default.post( @@ -186,13 +148,7 @@ extension CbfClient { while true { if Task.isCancelled { break } do { - #if DEBUG - print("[Kyoto] calling nextLog()") - #endif let log = try await self.nextLog() - #if DEBUG - print("[Kyoto] nextLog() returned: \(log)") - #endif } catch is CancellationError { break } catch { diff --git a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift index b7d797b0..59aa95cc 100644 --- a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift +++ b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift @@ -63,24 +63,12 @@ extension BlockchainClient { return Self( sync: { request, _ in let components = try getOrCreateComponents() - #if DEBUG - print("[Kyoto][BDKService] calling update() (sync)") - #endif let upd = try await components.client.update() - #if DEBUG - print("[Kyoto][BDKService] update() returned (sync)") - #endif return upd }, fullScan: { request, stopGap, _ in let components = try getOrCreateComponents() - #if DEBUG - print("[Kyoto][BDKService] calling update() (fullScan)") - #endif let upd = try await components.client.update() - #if DEBUG - print("[Kyoto][BDKService] update() returned (fullScan)") - #endif return upd }, broadcast: { tx in @@ -169,9 +157,6 @@ private class BDKService { self.blockchainURL.isEmpty ? Constants.Config.Kyoto.getDefaultPeer(for: self.network) : self.blockchainURL - #if DEBUG - print("[BDKService] selecting Kyoto peer=\(peer)") - #endif self.blockchainClient = .kyoto(peer: peer) } case .electrum: diff --git a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift index cda8ca1f..91ebc264 100644 --- a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift @@ -57,10 +57,6 @@ class WalletViewModel { self?.inspectedScripts = inspected let fraction = total > 0 ? Float(inspected) / Float(total) : 0 self?.progress = fraction - #if DEBUG - let percent = Int((fraction * 100).rounded()) - print("[Esplora][VM] inspected=\(inspected)/\(total) fraction=\(String(format: "%.4f", fraction)) percent=\(percent)%") - #endif } } } @@ -72,9 +68,6 @@ class WalletViewModel { let progressPercent = UInt64(progress) self?.inspectedScripts = progressPercent self?.totalScripts = 100 - #if DEBUG - print("[Kyoto][VM] percent=\(Int(progress))%") - #endif } } } From bc35d67b030422d526a66107f525929d0b4f4d62 Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 14 Aug 2025 14:51:42 -0500 Subject: [PATCH 40/40] format --- .../Extensions/BDK+Extensions/CbfClient+Extensions.swift | 2 +- BDKSwiftExampleWallet/View Model/WalletViewModel.swift | 4 ++-- .../View/Home/ActivityHomeHeaderView.swift | 7 ++++--- BDKSwiftExampleWallet/View/WalletView.swift | 5 +++-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift index 4f31de25..062b2a2b 100644 --- a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift +++ b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift @@ -28,7 +28,7 @@ extension CbfClient { .build(wallet: wallet) components.node.run() - + components.client.startBackgroundMonitoring() return (client: components.client, node: components.node) diff --git a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift index 91ebc264..be5e2399 100644 --- a/BDKSwiftExampleWallet/View Model/WalletViewModel.swift +++ b/BDKSwiftExampleWallet/View Model/WalletViewModel.swift @@ -108,7 +108,7 @@ class WalletViewModel { if progress > 0 { self.isKyotoConnected = true } - + // Update sync state based on Kyoto progress if progress >= 100 { self.walletSyncState = .synced @@ -125,7 +125,7 @@ class WalletViewModel { ) { [weak self] notification in if let connected = notification.userInfo?["connected"] as? Bool { self?.isKyotoConnected = connected - + // When Kyoto connects, update sync state if needed if connected && self?.walletSyncState == .notStarted { // Check current progress to determine state diff --git a/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift b/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift index be0b1d68..0f9faa6c 100644 --- a/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift +++ b/BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift @@ -40,7 +40,7 @@ struct ActivityHomeHeaderView: View { } else if walletSyncState == .syncing { HStack { if isKyotoClient { - if progress < 100.0 { // Kyoto progress is percent + if progress < 100.0 { // Kyoto progress is percent if currentBlockHeight > 0 { Text("Block \(currentBlockHeight)") .padding(.trailing, -5.0) @@ -55,7 +55,7 @@ struct ActivityHomeHeaderView: View { .transition(.opacity) } } - } else if progress < 1.0 { // Esplora progress is fraction + } else if progress < 1.0 { // Esplora progress is fraction Text("\(inspectedScripts)") .padding(.trailing, -5.0) .fontWeight(.semibold) @@ -71,7 +71,8 @@ struct ActivityHomeHeaderView: View { } if !isKyotoClient || (isKyotoClient && progress > 0) { - let percent: Int = isKyotoClient + let percent: Int = + isKyotoClient ? Int(progress.rounded()) : Int((progress * 100).rounded()) HStack(spacing: 0) { diff --git a/BDKSwiftExampleWallet/View/WalletView.swift b/BDKSwiftExampleWallet/View/WalletView.swift index bd0c0d19..695a00c9 100644 --- a/BDKSwiftExampleWallet/View/WalletView.swift +++ b/BDKSwiftExampleWallet/View/WalletView.swift @@ -137,8 +137,9 @@ struct WalletView: View { .onAppear { // Seed height from AppStorage on first show to avoid displaying 0 when Kyoto is active if viewModel.isKyotoClient, - viewModel.currentBlockHeight == 0, - kyotoLastHeight > 0 { + viewModel.currentBlockHeight == 0, + kyotoLastHeight > 0 + { viewModel.currentBlockHeight = UInt32(kyotoLastHeight) } }