Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ struct CoreContentView: View {
@State private var dashPayLastSync: Date?
// Progress values come from PlatformWalletManager (polled from FFI each second)

/// 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 = ""

/// 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
Expand Down Expand Up @@ -172,6 +179,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)
Expand All @@ -185,6 +203,34 @@ var body: some View {
}
}
.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 = 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.")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment on lines 182 to +233

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’¬ Nitpick: [Carried forward β€” STILL VALID] Rescan control still has no UI or unit test coverage

The cumulative PR adds dialog, parsing, preset resolution, per-wallet iteration, and failure aggregation, all without UI or unit test coverage. Targeted unit tests on rescanReferenceTip/armRescan would have caught the two blocking defects above directly.

source: ['codex-general', 'sonnet5-general']

} header: {
Text("Core Sync Status")
}
Expand Down Expand Up @@ -854,6 +900,62 @@ 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)
}
Comment on lines +908 to +919

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ”΄ Blocking: [Carried forward β€” STILL VALID] Relative rescan presets collapse to a full rescan once SPV is stopped

armRescan(lastBlocks:) computes its 'last N blocks' target relative to rescanReferenceTip, which reads walletManager.spvProgress.{filters,filterHeaders,headers}?.currentHeight. Once SPV is stopped, SpvRuntime::stop() drops the client, sync_progress() returns None, and the FFI layer publishes an empty progress snapshot back through Swift β€” every sub-progress field becomes absent/0. rescanReferenceTip then falls through all three ?? 0 fallbacks to 0, so both 'Last 1,000 blocks' and 'Last 10,000 blocks' silently call armRescan(fromHeight: 0), behaving identically to 'Everything (from height 0)' with no indication to the user that their targeted partial rescan just became a full rescan.

Suggested change
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)
}
private func armRescan(lastBlocks: UInt32) {
let tip = rescanReferenceTip
guard tip > 0 else {
print("⚠️ Start SPV before using a relative rescan preset, or choose a specific height.")
return
}
armRescan(fromHeight: tip > lastBlocks ? tip - lastBlocks : 0)
}

source: ['codex-general', 'sonnet5-general']


/// 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
/// console (matching Start/Clear β€” no UI surface).
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
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 {
print("❌ Rescan failed for: \(failures.joined(separator: ", "))")
}
}
Comment on lines +932 to +952

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ”΄ Blocking: [Carried forward β€” STILL VALID] Rescan reports wallets as armed without verifying an actual rewind occurred

The per-wallet loop in armRescan(fromHeight:) treats every non-throwing spvRescanFilters call as 'armed', without comparing fromHeight against that wallet's current checkpoint. spv_rescan_filters_blocking unconditionally overwrites synced_height and returns success even when fromHeight is at or above the checkpoint, so no rewind actually occurs despite the reported success β€” and a later height applied to an already-behind wallet can skip pending scan work. Use walletManager.coreWalletState(for:) to gate the call on a strict rewind.

source: ['codex-general', 'sonnet5-general']


/// 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.
Expand Down
Loading