From 7ce4ed470ebefa743813da2e2f09495d13aa1025 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Mon, 13 Jul 2026 04:13:06 +0700 Subject: [PATCH 1/3] feat(swift-sdk): compact-filter rescan button in Core Sync Status Adds a Rescan button to SwiftExampleApp's Core Sync Status card that arms an SPV compact-filter rescan (via the spvRescanFilters API from #4099) for every wallet on the active network, with a height-choice dialog (last 1k/10k blocks, everything, or a custom height) and a status caption reporting the outcome. Co-Authored-By: Claude Fable 5 --- .../Core/Views/CoreContentView.swift | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift index 7d9f307f3e2..fcddd4acce8 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift @@ -21,6 +21,15 @@ struct CoreContentView: View { @State private var dashPayLastSync: Date? // Progress values come from PlatformWalletManager (polled from FFI each second) + /// Rescan controls: the height-choice dialog, the follow-up + /// custom-height alert + its numeric field, and a transient + /// caption summarizing the last arm attempt (the Core section + /// has no other feedback surface — Start/Clear only `print`). + @State private var showRescanDialog = false + @State private var showCustomHeightAlert = false + @State private var customHeightText = "" + @State private var rescanStatus: String? + /// All persisted platform addresses across every wallet. Summed /// directly here so the global Sync Status view survives app /// restarts / wallet reconfigures without depending on the @@ -172,6 +181,17 @@ var body: some View { .tint(isSpvRunning ? .orange : .blue) .controlSize(.mini) + Button(action: { showRescanDialog = true }) { + Text("Rescan") + .font(.caption) + .fontWeight(.medium) + } + .buttonStyle(.borderedProminent) + .tint(.indigo) + .controlSize(.mini) + .disabled(walletIdsOnNetwork.isEmpty) + .accessibilityIdentifier("coreSync.rescanFiltersButton") + Button(action: clearSyncData) { Text("Clear") .font(.caption) @@ -183,8 +203,47 @@ var body: some View { .disabled(isSpvRunning) .opacity(isSpvRunning ? 0.5 : 1.0) } + + // Last rescan-arm outcome (success summary or the + // per-wallet failures). Cleared on the next arm. + if let rescanStatus { + Text(rescanStatus) + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(3) + .frame(maxWidth: .infinity, alignment: .leading) + } } .padding(.vertical, 4) + .confirmationDialog( + "Rescan Compact Filters", + isPresented: $showRescanDialog, + titleVisibility: .visible + ) { + Button("Last 1,000 blocks") { armRescan(lastBlocks: 1_000) } + Button("Last 10,000 blocks") { armRescan(lastBlocks: 10_000) } + Button("Everything (from height 0)") { armRescan(fromHeight: 0) } + Button("Custom height…") { showCustomHeightAlert = true } + Button("Cancel", role: .cancel) {} + } message: { + Text("Rewind the filter scan to re-download and re-match " + + "compact filters for every wallet on this network.") + } + .alert("Rescan From Height", isPresented: $showCustomHeightAlert) { + TextField("Block height", text: $customHeightText) + .keyboardType(.numberPad) + Button("Rescan") { + if let height = UInt32( + customHeightText.trimmingCharacters(in: .whitespaces) + ) { + armRescan(fromHeight: height) + } + customHeightText = "" + } + Button("Cancel", role: .cancel) { customHeightText = "" } + } message: { + Text("Enter the core block height to rescan compact filters from.") + } } header: { Text("Core Sync Status") } @@ -854,6 +913,58 @@ var body: some View { } } + // MARK: - Rescan + + /// Height the "last N blocks" rescan choices count back from: + /// filter-scan tip, falling back to filter-headers then headers + /// when the earlier stages haven't produced a height yet. + private var rescanReferenceTip: UInt32 { + let filters = walletManager.spvProgress.filters?.currentHeight ?? 0 + if filters > 0 { return filters } + let filterHeaders = walletManager.spvProgress.filterHeaders?.currentHeight ?? 0 + if filterHeaders > 0 { return filterHeaders } + return walletManager.spvProgress.headers?.currentHeight ?? 0 + } + + private func armRescan(lastBlocks: UInt32) { + let tip = rescanReferenceTip + armRescan(fromHeight: tip > lastBlocks ? tip - lastBlocks : 0) + } + + /// Rewind the filter-scan checkpoint to `fromHeight` for every + /// wallet on the active network. Collects per-wallet failures + /// instead of aborting on the first, then reports the outcome in + /// `rescanStatus`. + private func armRescan(fromHeight: UInt32) { + let ids = walletIdsOnNetwork + guard !ids.isEmpty else { return } + + var failures: [String] = [] + for walletId in ids { + do { + try walletManager.spvRescanFilters(walletId: walletId, fromHeight: fromHeight) + } catch { + failures.append("\(rescanShortId(walletId)): \(error.localizedDescription)") + } + } + + let armed = ids.count - failures.count + var msg = "Rescan armed from height \(fromHeight.formatted()) " + + "for \(armed) wallet\(armed == 1 ? "" : "s")" + if !isSpvRunning { + msg += " — applies on next SPV start" + } + if !failures.isEmpty { + msg += ". Failed: " + failures.joined(separator: ", ") + } + rescanStatus = msg + } + + /// First 4 bytes of a wallet id as hex, for compact failure lines. + private func rescanShortId(_ walletId: Data) -> String { + walletId.prefix(4).map { String(format: "%02x", $0) }.joined() + } + @MainActor private func refreshMasternodeFlag() async { // Best-effort: honor trusted mode reported by the Platform SDK. From f894a3361d1774f795b8c818359ad5d641b8311f Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Mon, 13 Jul 2026 04:35:29 +0700 Subject: [PATCH 2/3] refactor(swift-sdk): drop the rescan status caption, log outcome instead Co-Authored-By: Claude Fable 5 --- .../Core/Views/CoreContentView.swift | 33 +++++-------------- 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift index fcddd4acce8..6907f848088 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift @@ -21,14 +21,12 @@ struct CoreContentView: View { @State private var dashPayLastSync: Date? // Progress values come from PlatformWalletManager (polled from FFI each second) - /// Rescan controls: the height-choice dialog, the follow-up - /// custom-height alert + its numeric field, and a transient - /// caption summarizing the last arm attempt (the Core section - /// has no other feedback surface — Start/Clear only `print`). + /// Rescan controls: the height-choice dialog and the follow-up + /// custom-height alert + its numeric field. The arm outcome is + /// logged to the console, matching Start/Clear — no UI surface. @State private var showRescanDialog = false @State private var showCustomHeightAlert = false @State private var customHeightText = "" - @State private var rescanStatus: String? /// All persisted platform addresses across every wallet. Summed /// directly here so the global Sync Status view survives app @@ -203,16 +201,6 @@ var body: some View { .disabled(isSpvRunning) .opacity(isSpvRunning ? 0.5 : 1.0) } - - // Last rescan-arm outcome (success summary or the - // per-wallet failures). Cleared on the next arm. - if let rescanStatus { - Text(rescanStatus) - .font(.caption) - .foregroundColor(.secondary) - .lineLimit(3) - .frame(maxWidth: .infinity, alignment: .leading) - } } .padding(.vertical, 4) .confirmationDialog( @@ -933,8 +921,8 @@ var body: some View { /// Rewind the filter-scan checkpoint to `fromHeight` for every /// wallet on the active network. Collects per-wallet failures - /// instead of aborting on the first, then reports the outcome in - /// `rescanStatus`. + /// instead of aborting on the first, then logs the outcome to the + /// console (matching Start/Clear — no UI surface). private func armRescan(fromHeight: UInt32) { let ids = walletIdsOnNetwork guard !ids.isEmpty else { return } @@ -949,15 +937,12 @@ var body: some View { } let armed = ids.count - failures.count - var msg = "Rescan armed from height \(fromHeight.formatted()) " - + "for \(armed) wallet\(armed == 1 ? "" : "s")" - if !isSpvRunning { - msg += " — applies on next SPV start" - } + let applied = isSpvRunning ? "" : " (applies on next SPV start)" + print("🔁 Rescan armed from height \(fromHeight.formatted()) " + + "for \(armed) wallet\(armed == 1 ? "" : "s")\(applied)") if !failures.isEmpty { - msg += ". Failed: " + failures.joined(separator: ", ") + print("❌ Rescan failed for: \(failures.joined(separator: ", "))") } - rescanStatus = msg } /// First 4 bytes of a wallet id as hex, for compact failure lines. From f08071d5b591e10389c4e695cebffb47f07a23b1 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Mon, 13 Jul 2026 05:12:11 +0700 Subject: [PATCH 3/3] fix(swift-sdk): disable custom-height Rescan until the input parses Co-Authored-By: Claude Fable 5 --- .../SwiftExampleApp/Core/Views/CoreContentView.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift index 6907f848088..b64c9bace8b 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift @@ -221,13 +221,12 @@ var body: some View { TextField("Block height", text: $customHeightText) .keyboardType(.numberPad) Button("Rescan") { - if let height = UInt32( - customHeightText.trimmingCharacters(in: .whitespaces) - ) { + if let height = customHeightValue { armRescan(fromHeight: height) } customHeightText = "" } + .disabled(customHeightValue == nil) Button("Cancel", role: .cancel) { customHeightText = "" } } message: { Text("Enter the core block height to rescan compact filters from.") @@ -919,6 +918,13 @@ var body: some View { armRescan(fromHeight: tip > lastBlocks ? tip - lastBlocks : 0) } + /// The custom-height field parsed as a block height, or `nil` when + /// blank / non-numeric. Shared by the alert's disabled check and + /// its action so the two can't drift out of sync. + private var customHeightValue: UInt32? { + UInt32(customHeightText.trimmingCharacters(in: .whitespaces)) + } + /// Rewind the filter-scan checkpoint to `fromHeight` for every /// wallet on the active network. Collects per-wallet failures /// instead of aborting on the first, then logs the outcome to the