Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion BDKSwiftExampleWallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1106,7 +1106,7 @@
repositoryURL = "https://github.com/bitcoindevkit/bdk-swift";
requirement = {
kind = exactVersion;
version = 2.0.0;
version = 2.2.0;
};
};
AEAF83B42B7BD4D10019B23B /* XCRemoteSwiftPackageReference "CodeScanner" */ = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,26 @@ extension CbfClient {
// Track monitoring tasks per client for clean cancellation
private static var monitoringTasks: [ObjectIdentifier: Task<Void, Never>] = [:]
private static var warningTasks: [ObjectIdentifier: Task<Void, Never>] = [:]
private static var logTasks: [ObjectIdentifier: Task<Void, Never>] = [:]
private static var heartbeatTasks: [ObjectIdentifier: Task<Void, Never>] = [:]
private static var lastInfoAt: [ObjectIdentifier: Date] = [:]
private static let monitoringTasksQueue = DispatchQueue(label: "cbf.monitoring.tasks")

static func createComponents(wallet: Wallet) -> (client: CbfClient, node: CbfNode) {
static func createComponents(
wallet: Wallet,
scanType: ScanType,
peers: [Peer]
) -> (client: CbfClient, node: CbfNode) {
do {
let network = wallet.network()
let dataDir = Constants.Config.Kyoto.dbPath
print(
"[Kyoto] Preparing CBF components – network: \(network), dataDir: \(dataDir), peers: \(peers.count), scanType: \(scanType)"
)

let components = try CbfBuilder()
.logLevel(logLevel: .debug)
.scanType(scanType: .sync)
.dataDir(dataDir: Constants.Config.Kyoto.dbPath)
.peers(peers: Constants.Networks.Signet.Regular.kyotoPeers)
.scanType(scanType: scanType)
.dataDir(dataDir: dataDir)
.peers(peers: peers)
.build(wallet: wallet)

components.node.run()
Expand All @@ -47,34 +54,29 @@ extension CbfClient {
let info = try await self.nextInfo()
CbfClient.monitoringTasksQueue.sync { Self.lastInfoAt[id] = Date() }
switch info {
case .progress(let progress):
case .progress(let chainHeight, let filtersDownloadedPercent):
await MainActor.run {
NotificationCenter.default.post(
name: NSNotification.Name("KyotoProgressUpdate"),
object: nil,
userInfo: ["progress": progress]
userInfo: [
"progress": filtersDownloadedPercent,
"height": Int(chainHeight),
]
)
}
case .newChainHeight(let height):
await MainActor.run {
NotificationCenter.default.post(
name: NSNotification.Name("KyotoChainHeightUpdate"),
object: nil,
userInfo: ["height": height]
userInfo: ["height": Int(chainHeight)]
)
NotificationCenter.default.post(
name: NSNotification.Name("KyotoConnectionUpdate"),
object: nil,
userInfo: ["connected": true]
)
}
case .stateUpdate(let nodeState):
case .blockReceived(_):
await MainActor.run {
NotificationCenter.default.post(
name: NSNotification.Name("KyotoStateUpdate"),
object: nil,
userInfo: ["state": nodeState]
)
NotificationCenter.default.post(
name: NSNotification.Name("KyotoConnectionUpdate"),
object: nil,
Expand All @@ -89,8 +91,6 @@ extension CbfClient {
userInfo: ["connected": true]
)
}
default:
break
}
} catch is CancellationError {
break
Expand Down Expand Up @@ -149,23 +149,6 @@ extension CbfClient {
Self.warningTasks[id] = warnings
}

// Log listener for detailed debugging
let logs = Task { [self] in
while true {
if Task.isCancelled { break }
do {
let log = try await self.nextLog()
} catch is CancellationError {
break
} catch {
// ignore
}
}
}

Self.monitoringTasksQueue.sync {
Self.logTasks[id] = logs
}
}

func stopBackgroundMonitoring() {
Expand All @@ -175,7 +158,6 @@ extension CbfClient {
task.cancel()
if let hb = Self.heartbeatTasks.removeValue(forKey: id) { hb.cancel() }
if let wt = Self.warningTasks.removeValue(forKey: id) { wt.cancel() }
if let lt = Self.logTasks.removeValue(forKey: id) { lt.cancel() }
Self.lastInfoAt.removeValue(forKey: id)
}
}
Expand All @@ -184,11 +166,9 @@ extension CbfClient {
Self.monitoringTasksQueue.sync {
for (_, task) in Self.monitoringTasks { task.cancel() }
for (_, wt) in Self.warningTasks { wt.cancel() }
for (_, lt) in Self.logTasks { lt.cancel() }
for (_, hb) in Self.heartbeatTasks { hb.cancel() }
Self.monitoringTasks.removeAll()
Self.warningTasks.removeAll()
Self.logTasks.removeAll()
Self.heartbeatTasks.removeAll()
Self.lastInfoAt.removeAll()
}
Expand Down
25 changes: 21 additions & 4 deletions BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ enum BlockchainClientType: String, CaseIterable {
struct BlockchainClient {
let sync: @Sendable (SyncRequest, UInt64) async throws -> Update
let fullScan: @Sendable (FullScanRequest, UInt64, UInt64) async throws -> Update
let broadcast: @Sendable (Transaction) throws -> Void
let broadcast: @Sendable (Transaction) async throws -> Void
let getUrl: @Sendable () -> String
let getType: @Sendable () -> BlockchainClientType
let supportsFullScan: @Sendable () -> Bool = { true }
Expand Down Expand Up @@ -55,7 +55,24 @@ extension BlockchainClient {

try FileManager.default.ensureDirectoryExists(at: Constants.Config.Kyoto.dbDirectoryURL)

let components = CbfClient.createComponents(wallet: wallet)
let scanType: ScanType
if BDKService.shared.needsFullScanOfWallet() {
let addressType = BDKService.shared.getAddressType()
let checkpoint: RecoveryPoint =
addressType == .bip86 ? .taprootActivation : .segwitActivation
scanType = .recovery(
usedScriptIndex: 1000,
checkpoint: checkpoint
)
} else {
scanType = .sync
}

let components = CbfClient.createComponents(
wallet: wallet,
scanType: scanType,
peers: Constants.Networks.Signet.Regular.kyotoPeers
)
cbfComponents = components
return components
}
Expand All @@ -73,7 +90,7 @@ extension BlockchainClient {
},
broadcast: { tx in
let components = try getOrCreateComponents()
try components.client.broadcast(transaction: tx)
try await components.client.broadcast(transaction: tx)
},
getUrl: { peer },
getType: { .kyoto }
Expand Down Expand Up @@ -556,7 +573,7 @@ private class BDKService {
let isSigned = try wallet.sign(psbt: psbt)
if isSigned {
let transaction = try psbt.extractTx()
try self.blockchainClient.broadcast(transaction)
try await self.blockchainClient.broadcast(transaction)

if self.clientType == .kyoto {
let lastSeen = UInt64(Date().timeIntervalSince1970)
Expand Down
25 changes: 5 additions & 20 deletions BDKSwiftExampleWallet/View Model/WalletViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ class WalletViewModel {
}
var isKyotoConnected: Bool = false
var currentBlockHeight: UInt32 = 0
var kyotoNodeState: NodeState?

private var updateProgress: @Sendable (UInt64, UInt64) -> Void {
{ [weak self] inspected, total in
Expand Down Expand Up @@ -105,6 +104,9 @@ class WalletViewModel {
if self.bdkClient.getClientType() != .kyoto { return }
if let progress = notification.userInfo?["progress"] as? Float {
self.updateKyotoProgress(progress)
if let height = notification.userInfo?["height"] as? Int {
self.currentBlockHeight = UInt32(max(height, 0))
}
// Consider any progress update as evidence of an active connection
// so the UI does not falsely show a red disconnected indicator while syncing.
if progress > 0 {
Expand Down Expand Up @@ -148,8 +150,8 @@ class WalletViewModel {
guard let self else { return }
// Ignore Kyoto updates unless client type is Kyoto
if self.bdkClient.getClientType() != .kyoto { return }
if let height = notification.userInfo?["height"] as? UInt32 {
self.currentBlockHeight = height
if let height = notification.userInfo?["height"] as? Int {
self.currentBlockHeight = UInt32(max(height, 0))
// Receiving chain height implies we have peer connectivity
self.isKyotoConnected = true
// Ensure UI reflects syncing as soon as we see chain activity
Expand All @@ -162,23 +164,6 @@ class WalletViewModel {
}
}
}

NotificationCenter.default.addObserver(
forName: NSNotification.Name("KyotoStateUpdate"),
object: nil,
queue: .main
) { [weak self] notification in
guard let self else { return }
if self.bdkClient.getClientType() != .kyoto { return }
if let nodeState = notification.userInfo?["state"] as? NodeState {
self.kyotoNodeState = nodeState
if nodeState == .transactionsSynced {
self.walletSyncState = .synced
} else {
self.walletSyncState = .syncing
}
}
}
}

private func fullScanWithProgress() async {
Expand Down
26 changes: 6 additions & 20 deletions BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
// Created by Rubens Machion on 24/04/25.
//

import BitcoinDevKit
import SwiftUI

struct ActivityHomeHeaderView: View {
Expand All @@ -18,7 +17,6 @@ struct ActivityHomeHeaderView: View {
let isKyotoClient: Bool
let isKyotoConnected: Bool
let currentBlockHeight: UInt32
let kyotoNodeState: NodeState?

let showAllTransactions: () -> Void

Expand Down Expand Up @@ -209,25 +207,13 @@ struct ActivityHomeHeaderView: View {

extension ActivityHomeHeaderView {
fileprivate var kyotoStatusText: String? {
guard isKyotoClient, let kyotoNodeState else { return nil }
// Kyoto's NodeState reflects the next stage it will enter, so describe upcoming work.
switch kyotoNodeState {
case .behind:
// Still acquiring header tips, so call out the header sync explicitly.
return "Getting headers..."
case .headersSynced:
// Kyoto reports this once headers are already finished, so surface the next
// actionable phase the node is entering rather than the completed step.
return "Preparing filters..."
case .filterHeadersSynced:
// Filter headers are ready; actual filter scanning starts next.
return "Scanning filters..."
case .filtersSynced:
// Filters are exhausted; the node now gossips for matching blocks/txs.
return "Fetching matches..."
case .transactionsSynced:
// No further phases—fall back to showing percent + standard synced UI.
guard isKyotoClient else { return nil }
if walletSyncState == .synced || progress >= 100 {
return nil
}
if progress <= 0 {
return isKyotoConnected ? "Getting headers..." : "Connecting..."
}
return "Scanning filters..."
}
}
3 changes: 1 addition & 2 deletions BDKSwiftExampleWallet/View/WalletView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@ struct WalletView: View {
needsFullScan: viewModel.needsFullScan,
isKyotoClient: viewModel.isKyotoClient,
isKyotoConnected: viewModel.isKyotoConnected,
currentBlockHeight: viewModel.currentBlockHeight,
kyotoNodeState: viewModel.kyotoNodeState
currentBlockHeight: viewModel.currentBlockHeight
) {
showAllTransactions = true
}
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ Download the app on [TestFlight](https://testflight.apple.com/join/A3nAuYvZ).

## Build

### BDK 1.0
### BDK Version

The `main` branch of BDK Swift Example Wallet uses [bdk-swift](https://github.com/bitcoindevkit/bdk-swift) 1.0+.
The `main` branch of BDK Swift Example Wallet uses [bdk-swift](https://github.com/bitcoindevkit/bdk-swift) 2.0+.

## Functionality

Expand Down