Skip to content

Commit de4e9ca

Browse files
authored
feat: kyoto
1 parent 3317d2b commit de4e9ca

File tree

16 files changed

+829
-156
lines changed

16 files changed

+829
-156
lines changed

BDKSwiftExampleWallet.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
AE4984832A1BBBD7009951E2 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AE4984822A1BBBD7009951E2 /* Preview Assets.xcassets */; };
5555
AE49848D2A1BBBD8009951E2 /* BDKSwiftExampleWalletTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE49848C2A1BBBD8009951E2 /* BDKSwiftExampleWalletTests.swift */; };
5656
AE4984A62A1BBCB8009951E2 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = AE4984A52A1BBCB8009951E2 /* README.md */; };
57+
AE4D97572E3AFF2500E88A38 /* CbfClient+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE4D97562E3AFF2500E88A38 /* CbfClient+Extensions.swift */; };
5758
AE6715FA2A9A9220005C193F /* BDKSwiftExampleWalletPriceServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE6715F92A9A9220005C193F /* BDKSwiftExampleWalletPriceServiceTests.swift */; };
5859
AE6715FD2A9AC056005C193F /* PriceServiceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE6715FC2A9AC056005C193F /* PriceServiceError.swift */; };
5960
AE6715FF2A9AC066005C193F /* FeeServiceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE6715FE2A9AC066005C193F /* FeeServiceError.swift */; };
@@ -163,6 +164,7 @@
163164
AE4984882A1BBBD8009951E2 /* BDKSwiftExampleWalletTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BDKSwiftExampleWalletTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
164165
AE49848C2A1BBBD8009951E2 /* BDKSwiftExampleWalletTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BDKSwiftExampleWalletTests.swift; sourceTree = "<group>"; };
165166
AE4984A52A1BBCB8009951E2 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
167+
AE4D97562E3AFF2500E88A38 /* CbfClient+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CbfClient+Extensions.swift"; sourceTree = "<group>"; };
166168
AE6474732CE559E000A270C6 /* BDKSwiftExampleWallet.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = BDKSwiftExampleWallet.entitlements; sourceTree = "<group>"; };
167169
AE6715F92A9A9220005C193F /* BDKSwiftExampleWalletPriceServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BDKSwiftExampleWalletPriceServiceTests.swift; sourceTree = "<group>"; };
168170
AE6715FC2A9AC056005C193F /* PriceServiceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceServiceError.swift; sourceTree = "<group>"; };
@@ -572,6 +574,7 @@
572574
isa = PBXGroup;
573575
children = (
574576
77EDA65A2E2A5B3800A5E3AD /* URL+Extensions.swift */,
577+
AE4D97562E3AFF2500E88A38 /* CbfClient+Extensions.swift */,
575578
AE97E74C2E315A8F000A407D /* AddressType+Extensions.swift */,
576579
77F0FDC82DA9A93700B30E4F /* Persister+Extensions.swift */,
577580
AEE6C74B2ABCB3E200442ADD /* Transaction+Extensions.swift */,
@@ -756,6 +759,7 @@
756759
AE73239B2DF9C00F00D9BAE2 /* TxId+Extensions.swift in Sources */,
757760
AE1390C72A7DB0AF0098127A /* KeyService.swift in Sources */,
758761
AED4CC0A2A1D297600CE1831 /* BDKService.swift in Sources */,
762+
AE4D97572E3AFF2500E88A38 /* CbfClient+Extensions.swift in Sources */,
759763
AED4CC102A1D522100CE1831 /* WalletView.swift in Sources */,
760764
AE7F67092A7451AA00CED561 /* Price.swift in Sources */,
761765
AE184EFC2BFE52C800374362 /* Amount+Extensions.swift in Sources */,
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
//
2+
// CbfClient+Extensions.swift
3+
// BDKSwiftExampleWallet
4+
//
5+
// Created by Matthew Ramsden on 7/30/25.
6+
//
7+
8+
import BitcoinDevKit
9+
import Foundation
10+
11+
extension CbfClient {
12+
// Track monitoring tasks per client for clean cancellation
13+
private static var monitoringTasks: [ObjectIdentifier: Task<Void, Never>] = [:]
14+
private static var warningTasks: [ObjectIdentifier: Task<Void, Never>] = [:]
15+
private static var logTasks: [ObjectIdentifier: Task<Void, Never>] = [:]
16+
private static var heartbeatTasks: [ObjectIdentifier: Task<Void, Never>] = [:]
17+
private static var lastInfoAt: [ObjectIdentifier: Date] = [:]
18+
private static let monitoringTasksQueue = DispatchQueue(label: "cbf.monitoring.tasks")
19+
20+
static func createComponents(wallet: Wallet) -> (client: CbfClient, node: CbfNode) {
21+
do {
22+
23+
let components = try CbfBuilder()
24+
.logLevel(logLevel: .debug)
25+
.scanType(scanType: .sync)
26+
.dataDir(dataDir: Constants.Config.Kyoto.dbPath)
27+
.peers(peers: Constants.Networks.Signet.Regular.kyotoPeers)
28+
.build(wallet: wallet)
29+
30+
components.node.run()
31+
32+
components.client.startBackgroundMonitoring()
33+
34+
return (client: components.client, node: components.node)
35+
} catch {
36+
fatalError("Failed to create CBF components: \(error)")
37+
}
38+
}
39+
40+
func startBackgroundMonitoring() {
41+
let id = ObjectIdentifier(self)
42+
43+
let task = Task { [self] in
44+
var hasEstablishedConnection = false
45+
while true {
46+
if Task.isCancelled { break }
47+
do {
48+
let info = try await self.nextInfo()
49+
CbfClient.monitoringTasksQueue.sync { Self.lastInfoAt[id] = Date() }
50+
switch info {
51+
case let .progress(progress):
52+
await MainActor.run {
53+
NotificationCenter.default.post(
54+
name: NSNotification.Name("KyotoProgressUpdate"),
55+
object: nil,
56+
userInfo: ["progress": progress]
57+
)
58+
}
59+
case let .newChainHeight(height):
60+
await MainActor.run {
61+
NotificationCenter.default.post(
62+
name: NSNotification.Name("KyotoChainHeightUpdate"),
63+
object: nil,
64+
userInfo: ["height": height]
65+
)
66+
if !hasEstablishedConnection {
67+
hasEstablishedConnection = true
68+
NotificationCenter.default.post(
69+
name: NSNotification.Name("KyotoConnectionUpdate"),
70+
object: nil,
71+
userInfo: ["connected": true]
72+
)
73+
}
74+
}
75+
case .connectionsMet, .successfulHandshake:
76+
await MainActor.run {
77+
if !hasEstablishedConnection {
78+
hasEstablishedConnection = true
79+
NotificationCenter.default.post(
80+
name: NSNotification.Name("KyotoConnectionUpdate"),
81+
object: nil,
82+
userInfo: ["connected": true]
83+
)
84+
}
85+
}
86+
default:
87+
break
88+
}
89+
} catch is CancellationError {
90+
break
91+
} catch {
92+
// ignore
93+
}
94+
}
95+
}
96+
97+
Self.monitoringTasksQueue.sync {
98+
Self.monitoringTasks[id] = task
99+
Self.lastInfoAt[id] = Date()
100+
}
101+
102+
// Heartbeat task to signal idleness while awaiting Info events
103+
let heartbeat = Task {
104+
while true {
105+
if Task.isCancelled { break }
106+
try? await Task.sleep(nanoseconds: 5_000_000_000)
107+
if Task.isCancelled { break }
108+
var idleFor: TimeInterval = 0
109+
CbfClient.monitoringTasksQueue.sync {
110+
if let last = Self.lastInfoAt[id] { idleFor = Date().timeIntervalSince(last) }
111+
}
112+
}
113+
}
114+
115+
Self.monitoringTasksQueue.sync {
116+
Self.heartbeatTasks[id] = heartbeat
117+
}
118+
119+
// Minimal warnings listener for visibility while syncing
120+
let warnings = Task { [self] in
121+
while true {
122+
if Task.isCancelled { break }
123+
do {
124+
let warning = try await self.nextWarning()
125+
if case .needConnections = warning {
126+
await MainActor.run {
127+
NotificationCenter.default.post(
128+
name: NSNotification.Name("KyotoConnectionUpdate"),
129+
object: nil,
130+
userInfo: ["connected": false]
131+
)
132+
}
133+
}
134+
} catch is CancellationError {
135+
break
136+
} catch {
137+
// ignore
138+
}
139+
}
140+
}
141+
142+
Self.monitoringTasksQueue.sync {
143+
Self.warningTasks[id] = warnings
144+
}
145+
146+
// Log listener for detailed debugging
147+
let logs = Task { [self] in
148+
while true {
149+
if Task.isCancelled { break }
150+
do {
151+
let log = try await self.nextLog()
152+
} catch is CancellationError {
153+
break
154+
} catch {
155+
// ignore
156+
}
157+
}
158+
}
159+
160+
Self.monitoringTasksQueue.sync {
161+
Self.logTasks[id] = logs
162+
}
163+
}
164+
165+
func stopBackgroundMonitoring() {
166+
let id = ObjectIdentifier(self)
167+
Self.monitoringTasksQueue.sync {
168+
guard let task = Self.monitoringTasks.removeValue(forKey: id) else { return }
169+
task.cancel()
170+
if let hb = Self.heartbeatTasks.removeValue(forKey: id) { hb.cancel() }
171+
if let wt = Self.warningTasks.removeValue(forKey: id) { wt.cancel() }
172+
if let lt = Self.logTasks.removeValue(forKey: id) { lt.cancel() }
173+
Self.lastInfoAt.removeValue(forKey: id)
174+
}
175+
}
176+
177+
static func cancelAllMonitoring() {
178+
Self.monitoringTasksQueue.sync {
179+
for (_, task) in Self.monitoringTasks { task.cancel() }
180+
for (_, wt) in Self.warningTasks { wt.cancel() }
181+
for (_, lt) in Self.logTasks { lt.cancel() }
182+
for (_, hb) in Self.heartbeatTasks { hb.cancel() }
183+
Self.monitoringTasks.removeAll()
184+
Self.warningTasks.removeAll()
185+
Self.logTasks.removeAll()
186+
Self.heartbeatTasks.removeAll()
187+
Self.lastInfoAt.removeAll()
188+
}
189+
}
190+
}

BDKSwiftExampleWallet/Resources/Localizable.xcstrings

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,9 @@
146146
}
147147
}
148148
}
149+
},
150+
"%" : {
151+
149152
},
150153
"%@ • %@" : {
151154
"localizations" : {
@@ -210,6 +213,9 @@
210213
}
211214
}
212215
}
216+
},
217+
"%lld" : {
218+
213219
},
214220
"%lld Output%@" : {
215221
"localizations" : {
@@ -467,6 +473,9 @@
467473
}
468474
}
469475
}
476+
},
477+
"Block %u" : {
478+
470479
},
471480
"Build Transaction Error" : {
472481
"localizations" : {
@@ -483,6 +492,9 @@
483492
}
484493
}
485494
}
495+
},
496+
"Client" : {
497+
486498
},
487499
"Coldcard Verify Address" : {
488500

@@ -623,6 +635,9 @@
623635
}
624636
}
625637
}
638+
},
639+
"Esplora" : {
640+
626641
},
627642
"Esplora Server" : {
628643
"localizations" : {
@@ -733,6 +748,9 @@
733748
}
734749
}
735750
}
751+
},
752+
"Kyoto" : {
753+
736754
},
737755
"Navigation Title" : {
738756
"extractionState" : "stale",
@@ -1049,6 +1067,9 @@
10491067
}
10501068
}
10511069
}
1070+
},
1071+
"Select Client Type" : {
1072+
10521073
},
10531074
"Select Fee" : {
10541075
"localizations" : {

0 commit comments

Comments
 (0)