From eaff22b0ace4a35cf1a40c90c2c7ba5524edf8e0 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 3 Nov 2025 14:18:26 -0600 Subject: [PATCH 1/2] wip: issue-320 --- .../BDK+Extensions/CbfClient+Extensions.swift | 12 +++++- .../Service/BDK Service/BDKService.swift | 40 ++++++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift index 4a5ec154..dec41988 100644 --- a/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift +++ b/BDKSwiftExampleWallet/Extensions/BDK+Extensions/CbfClient+Extensions.swift @@ -128,7 +128,8 @@ extension CbfClient { if Task.isCancelled { break } do { let warning = try await self.nextWarning() - if case .needConnections = warning { + switch warning { + case .needConnections: await MainActor.run { NotificationCenter.default.post( name: NSNotification.Name("KyotoConnectionUpdate"), @@ -136,6 +137,15 @@ extension CbfClient { userInfo: ["connected": false] ) } + case let .transactionRejected(wtxid, reason): + BDKService.shared.handleKyotoRejectedTransaction(wtxidHex: wtxid) + if let reason { + print("Kyoto rejected tx \(wtxid): \(reason)") + } else { + print("Kyoto rejected tx \(wtxid)") + } + default: + break } } catch is CancellationError { break diff --git a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift index 39670b34..afccc81e 100644 --- a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift +++ b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift @@ -98,7 +98,7 @@ extension BlockchainClient { } } -private class BDKService { +final class BDKService { static let shared: BDKService = BDKService() private var balance: Balance? @@ -110,6 +110,8 @@ private class BDKService { private(set) var network: Network private var blockchainURL: String internal private(set) var wallet: Wallet? + private var kyotoPendingTxs: [String: Txid] = [:] + private let kyotoPendingTxQueue = DispatchQueue(label: "bdk.service.kyoto.pending") init(keyClient: KeyClient = .live) { self.keyClient = keyClient @@ -532,6 +534,7 @@ private class BDKService { try? keyClient.deleteEsplora() needsFullScan = true + clearKyotoTrackedTransactions() } func getBackupInfo() throws -> BackupInfo { @@ -576,6 +579,7 @@ private class BDKService { try await self.blockchainClient.broadcast(transaction) if self.clientType == .kyoto { + trackKyotoBroadcast(transaction) let lastSeen = UInt64(Date().timeIntervalSince1970) let unconfirmedTx = UnconfirmedTx(tx: transaction, lastSeen: lastSeen) wallet.applyUnconfirmedTxs(unconfirmedTxs: [unconfirmedTx]) @@ -589,6 +593,40 @@ private class BDKService { } } + private func trackKyotoBroadcast(_ transaction: Transaction) { + let wtxidData = transaction.computeWtxid().serialize() + let wtxidHex = [UInt8](wtxidData).hexString.lowercased() + let txid = transaction.computeTxid() + kyotoPendingTxQueue.sync { + kyotoPendingTxs[wtxidHex] = txid + } + } + + private func takeKyotoTx(for wtxidHex: String) -> Txid? { + kyotoPendingTxQueue.sync { + kyotoPendingTxs.removeValue(forKey: wtxidHex.lowercased()) + } + } + + private func clearKyotoTrackedTransactions() { + kyotoPendingTxQueue.sync { + kyotoPendingTxs.removeAll() + } + } + + func handleKyotoRejectedTransaction(wtxidHex: String) { + guard let txid = takeKyotoTx(for: wtxidHex) else { return } + guard let wallet = self.wallet else { return } + let evictedTx = EvictedTx( + txid: txid, + evictedAt: UInt64(Date().timeIntervalSince1970) + ) + wallet.applyEvictedTxs(evictedTxs: [evictedTx]) + if let persister = self.persister { + try? wallet.persist(persister: persister) + } + } + func syncWithInspector(inspector: SyncScriptInspector) async throws { guard let wallet = self.wallet else { throw WalletError.walletNotFound } let syncRequest = try wallet.startSyncWithRevealedSpks() From 8f0f18eaeefef47deb01271a7c08fe4da6a75af4 Mon Sep 17 00:00:00 2001 From: Matthew Date: Tue, 4 Nov 2025 08:38:35 -0600 Subject: [PATCH 2/2] refactor: use `Wtxid` as the key in the map --- .../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 afccc81e..cdd6ab80 100644 --- a/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift +++ b/BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift @@ -110,7 +110,7 @@ final class BDKService { private(set) var network: Network private var blockchainURL: String internal private(set) var wallet: Wallet? - private var kyotoPendingTxs: [String: Txid] = [:] + private var kyotoPendingTxs: [Wtxid: Txid] = [:] private let kyotoPendingTxQueue = DispatchQueue(label: "bdk.service.kyoto.pending") init(keyClient: KeyClient = .live) { @@ -594,17 +594,16 @@ final class BDKService { } private func trackKyotoBroadcast(_ transaction: Transaction) { - let wtxidData = transaction.computeWtxid().serialize() - let wtxidHex = [UInt8](wtxidData).hexString.lowercased() + let wtxid = transaction.computeWtxid() let txid = transaction.computeTxid() kyotoPendingTxQueue.sync { - kyotoPendingTxs[wtxidHex] = txid + kyotoPendingTxs[wtxid] = txid } } - private func takeKyotoTx(for wtxidHex: String) -> Txid? { + private func takeKyotoTx(for wtxid: Wtxid) -> Txid? { kyotoPendingTxQueue.sync { - kyotoPendingTxs.removeValue(forKey: wtxidHex.lowercased()) + kyotoPendingTxs.removeValue(forKey: wtxid) } } @@ -615,7 +614,8 @@ final class BDKService { } func handleKyotoRejectedTransaction(wtxidHex: String) { - guard let txid = takeKyotoTx(for: wtxidHex) else { return } + guard let wtxid = try? Wtxid.fromString(hex: wtxidHex.lowercased()) else { return } + guard let txid = takeKyotoTx(for: wtxid) else { return } guard let wallet = self.wallet else { return } let evictedTx = EvictedTx( txid: txid,