Skip to content

Commit a33e0d3

Browse files
authored
feat: kyoto follow on
* fix: clamp kyoto progress updates * feat: apply unconfirmed * feat: surface kyoto sync phase status * feat: refresh connection state on reconnect
1 parent 2da8841 commit a33e0d3

File tree

5 files changed

+94
-25
lines changed

5 files changed

+94
-25
lines changed

BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ extension CbfClient {
4141
let id = ObjectIdentifier(self)
4242

4343
let task = Task { [self] in
44-
var hasEstablishedConnection = false
4544
while true {
4645
if Task.isCancelled { break }
4746
do {
@@ -63,25 +62,32 @@ extension CbfClient {
6362
object: nil,
6463
userInfo: ["height": height]
6564
)
66-
if !hasEstablishedConnection {
67-
hasEstablishedConnection = true
68-
NotificationCenter.default.post(
69-
name: NSNotification.Name("KyotoConnectionUpdate"),
70-
object: nil,
71-
userInfo: ["connected": true]
72-
)
73-
}
65+
NotificationCenter.default.post(
66+
name: NSNotification.Name("KyotoConnectionUpdate"),
67+
object: nil,
68+
userInfo: ["connected": true]
69+
)
70+
}
71+
case .stateUpdate(let nodeState):
72+
await MainActor.run {
73+
NotificationCenter.default.post(
74+
name: NSNotification.Name("KyotoStateUpdate"),
75+
object: nil,
76+
userInfo: ["state": nodeState]
77+
)
78+
NotificationCenter.default.post(
79+
name: NSNotification.Name("KyotoConnectionUpdate"),
80+
object: nil,
81+
userInfo: ["connected": true]
82+
)
7483
}
7584
case .connectionsMet, .successfulHandshake:
7685
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-
}
86+
NotificationCenter.default.post(
87+
name: NSNotification.Name("KyotoConnectionUpdate"),
88+
object: nil,
89+
userInfo: ["connected": true]
90+
)
8591
}
8692
default:
8793
break

BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,16 @@ private class BDKService {
557557
if isSigned {
558558
let transaction = try psbt.extractTx()
559559
try self.blockchainClient.broadcast(transaction)
560+
561+
if self.clientType == .kyoto {
562+
let lastSeen = UInt64(Date().timeIntervalSince1970)
563+
let unconfirmedTx = UnconfirmedTx(tx: transaction, lastSeen: lastSeen)
564+
wallet.applyUnconfirmedTxs(unconfirmedTxs: [unconfirmedTx])
565+
guard let persister = self.persister else {
566+
throw WalletError.dbNotFound
567+
}
568+
let _ = try wallet.persist(persister: persister)
569+
}
560570
} else {
561571
throw WalletError.notSigned
562572
}

BDKSwiftExampleWallet/View Model/WalletViewModel.swift

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class WalletViewModel {
4747
}
4848
var isKyotoConnected: Bool = false
4949
var currentBlockHeight: UInt32 = 0
50+
var kyotoNodeState: NodeState?
5051

5152
private var updateProgress: @Sendable (UInt64, UInt64) -> Void {
5253
{ [weak self] inspected, total in
@@ -62,12 +63,13 @@ class WalletViewModel {
6263
}
6364

6465
private var updateKyotoProgress: @Sendable (Float) -> Void {
65-
{ [weak self] progress in
66-
DispatchQueue.main.async {
67-
self?.progress = progress
68-
let progressPercent = UInt64(progress)
69-
self?.inspectedScripts = progressPercent
70-
self?.totalScripts = 100
66+
{ [weak self] rawProgress in
67+
DispatchQueue.main.async { [weak self] in
68+
guard let self else { return }
69+
let sanitized = rawProgress.isFinite ? min(max(rawProgress, 0), 100) : 0
70+
self.progress = sanitized
71+
self.inspectedScripts = UInt64(sanitized)
72+
self.totalScripts = 100
7173
}
7274
}
7375
}
@@ -160,6 +162,23 @@ class WalletViewModel {
160162
}
161163
}
162164
}
165+
166+
NotificationCenter.default.addObserver(
167+
forName: NSNotification.Name("KyotoStateUpdate"),
168+
object: nil,
169+
queue: .main
170+
) { [weak self] notification in
171+
guard let self else { return }
172+
if self.bdkClient.getClientType() != .kyoto { return }
173+
if let nodeState = notification.userInfo?["state"] as? NodeState {
174+
self.kyotoNodeState = nodeState
175+
if nodeState == .transactionsSynced {
176+
self.walletSyncState = .synced
177+
} else {
178+
self.walletSyncState = .syncing
179+
}
180+
}
181+
}
163182
}
164183

165184
private func fullScanWithProgress() async {

BDKSwiftExampleWallet/View/Home/ActivityHomeHeaderView.swift

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Created by Rubens Machion on 24/04/25.
66
//
77

8+
import BitcoinDevKit
89
import SwiftUI
910

1011
struct ActivityHomeHeaderView: View {
@@ -17,6 +18,7 @@ struct ActivityHomeHeaderView: View {
1718
let isKyotoClient: Bool
1819
let isKyotoConnected: Bool
1920
let currentBlockHeight: UInt32
21+
let kyotoNodeState: NodeState?
2022

2123
let showAllTransactions: () -> Void
2224

@@ -40,7 +42,13 @@ struct ActivityHomeHeaderView: View {
4042
} else if walletSyncState == .syncing {
4143
HStack {
4244
if isKyotoClient {
43-
if progress < 100.0 { // Kyoto progress is percent
45+
if let status = kyotoStatusText {
46+
Text(status)
47+
.padding(.trailing, -5.0)
48+
.fontWeight(.semibold)
49+
.contentTransition(.opacity)
50+
.transition(.opacity)
51+
} else if progress < 100.0 { // Kyoto progress is percent
4452
if currentBlockHeight > 0 {
4553
Text("Block \(currentBlockHeight)")
4654
.padding(.trailing, -5.0)
@@ -198,3 +206,28 @@ struct ActivityHomeHeaderView: View {
198206
}
199207
}
200208
}
209+
210+
extension ActivityHomeHeaderView {
211+
fileprivate var kyotoStatusText: String? {
212+
guard isKyotoClient, let kyotoNodeState else { return nil }
213+
// Kyoto's NodeState reflects the next stage it will enter, so describe upcoming work.
214+
switch kyotoNodeState {
215+
case .behind:
216+
// Still acquiring header tips, so call out the header sync explicitly.
217+
return "Getting headers..."
218+
case .headersSynced:
219+
// Kyoto reports this once headers are already finished, so surface the next
220+
// actionable phase the node is entering rather than the completed step.
221+
return "Preparing filters..."
222+
case .filterHeadersSynced:
223+
// Filter headers are ready; actual filter scanning starts next.
224+
return "Scanning filters..."
225+
case .filtersSynced:
226+
// Filters are exhausted; the node now gossips for matching blocks/txs.
227+
return "Fetching matches..."
228+
case .transactionsSynced:
229+
// No further phases—fall back to showing percent + standard synced UI.
230+
return nil
231+
}
232+
}
233+
}

BDKSwiftExampleWallet/View/WalletView.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ struct WalletView: View {
5151
needsFullScan: viewModel.needsFullScan,
5252
isKyotoClient: viewModel.isKyotoClient,
5353
isKyotoConnected: viewModel.isKyotoConnected,
54-
currentBlockHeight: viewModel.currentBlockHeight
54+
currentBlockHeight: viewModel.currentBlockHeight,
55+
kyotoNodeState: viewModel.kyotoNodeState
5556
) {
5657
showAllTransactions = true
5758
}

0 commit comments

Comments
 (0)