Skip to content

Commit 5ff7d51

Browse files
committed
fix restore issues
1 parent 3c144d6 commit 5ff7d51

8 files changed

Lines changed: 158 additions & 10 deletions

File tree

Bitkit/AppScene.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,9 @@ struct AppScene: View {
439439
}
440440

441441
private func restoreFromMostRecentBackup() async {
442+
BackupService.shared.setRestoring(true)
443+
defer { BackupService.shared.setRestoring(false) }
444+
442445
guard let mnemonicData = try? Keychain.load(key: .bip39Mnemonic(index: 0)),
443446
let mnemonic = String(data: mnemonicData, encoding: .utf8)
444447
else { return }

Bitkit/Services/BackupService.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ class BackupService {
7676
}
7777

7878
func startObservingBackups() {
79+
guard !shouldSkipBackup() else { return }
80+
7981
Task {
8082
let shouldStart = try? await ServiceQueue.background(.backup) {
8183
guard !self.isObserving else { return false }
@@ -379,6 +381,10 @@ class BackupService {
379381
}
380382
}
381383

384+
func setRestoring(_ value: Bool) {
385+
stateQueue.sync { isRestoring = value }
386+
}
387+
382388
private func shouldSkipBackup() -> Bool {
383389
return stateQueue.sync {
384390
isRestoring || isWiping

Bitkit/Services/LightningService.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,11 @@ extension LightningService {
914914
}
915915
}
916916

917+
func listMonitoredAddressTypes() -> [LDKNode.AddressType] {
918+
guard let node else { return [] }
919+
return node.listMonitoredAddressTypes()
920+
}
921+
917922
func setPrimaryAddressType(_ addressType: LDKNode.AddressType) async throws {
918923
guard let node else {
919924
throw AppError(serviceError: .nodeNotSetup)

Bitkit/ViewModels/SettingsViewModel.swift

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ class SettingsViewModel: NSObject, ObservableObject {
4848

4949
private let defaults = UserDefaults.standard
5050

51-
private var isChangingAddressType = false
51+
@Published private(set) var isChangingAddressType = false
52+
/// Set during restore when backup contained explicit monitored address types.
53+
private(set) var restoredMonitoredTypesFromBackup = false
5254
private var observedKeys: Set<String> = []
5355

5456
// Reactive publishers for settings changes (used by BackupService)
@@ -211,6 +213,7 @@ class SettingsViewModel: NSObject, ObservableObject {
211213
_addressTypesToMonitor = "nativeSegwit"
212214
pinEnabled = false
213215
isChangingAddressType = false
216+
restoredMonitoredTypesFromBackup = false
214217
}
215218

216219
// MARK: - Computed Properties
@@ -275,6 +278,9 @@ class SettingsViewModel: NSObject, ObservableObject {
275278
}
276279

277280
// Address Type Settings
281+
/// Address types that support native SegWit scripts (required for Lightning).
282+
private static let nativeWitnessTypes: [AddressScriptType] = [.nativeSegwit, .taproot]
283+
278284
@AppStorage("selectedAddressType") private var _selectedAddressType: String = "nativeSegwit"
279285

280286
@AppStorage("addressTypesToMonitor") private var _addressTypesToMonitor: String = "nativeSegwit"
@@ -337,8 +343,7 @@ class SettingsViewModel: NSObject, ObservableObject {
337343
return false
338344
}
339345

340-
let nativeWitnessTypes: [AddressScriptType] = [.nativeSegwit, .taproot]
341-
let remainingNativeWitness = current.filter { $0 != addressType && nativeWitnessTypes.contains($0) }
346+
let remainingNativeWitness = current.filter { $0 != addressType && Self.nativeWitnessTypes.contains($0) }
342347
if remainingNativeWitness.isEmpty {
343348
return false
344349
}
@@ -372,6 +377,14 @@ class SettingsViewModel: NSObject, ObservableObject {
372377
addressTypesToMonitor = AddressScriptType.allAddressTypes
373378
}
374379

380+
/// Syncs monitored address types from ldk-node's runtime state into UserDefaults.
381+
func syncMonitoredTypesFromNode() {
382+
let nodeMonitored = lightningService.listMonitoredAddressTypes()
383+
var combined = Set(nodeMonitored)
384+
combined.insert(selectedAddressType)
385+
addressTypesToMonitor = AddressScriptType.allAddressTypes.filter { combined.contains($0) }
386+
}
387+
375388
private static let pendingRestoreAddressTypePruneKey = "pendingRestoreAddressTypePrune"
376389

377390
/// Tracks whether to prune empty address types after restore (set when user taps Get Started; cleared when prune runs).
@@ -385,7 +398,6 @@ class SettingsViewModel: NSObject, ObservableObject {
385398
func pruneEmptyAddressTypesAfterRestore() async {
386399
guard !isChangingAddressType else { return }
387400

388-
let nativeWitnessTypes: [AddressScriptType] = [.nativeSegwit, .taproot]
389401
var newMonitored = addressTypesToMonitor
390402
var changed = false
391403

@@ -407,7 +419,7 @@ class SettingsViewModel: NSObject, ObservableObject {
407419
}
408420

409421
// Ensure at least one native witness type
410-
if !newMonitored.contains(where: { nativeWitnessTypes.contains($0) }) {
422+
if !newMonitored.contains(where: { Self.nativeWitnessTypes.contains($0) }) {
411423
if !newMonitored.contains(.nativeSegwit) {
412424
newMonitored.append(.nativeSegwit)
413425
changed = true
@@ -438,10 +450,9 @@ class SettingsViewModel: NSObject, ObservableObject {
438450

439451
/// True if disabling this would leave no native witness wallet (required for Lightning).
440452
func isLastRequiredNativeWitnessWallet(_ addressType: AddressScriptType) -> Bool {
441-
let nativeWitnessTypes: [AddressScriptType] = [.nativeSegwit, .taproot]
442-
guard nativeWitnessTypes.contains(addressType) else { return false }
453+
guard Self.nativeWitnessTypes.contains(addressType) else { return false }
443454

444-
let remainingNativeWitness = addressTypesToMonitor.filter { $0 != addressType && nativeWitnessTypes.contains($0) }
455+
let remainingNativeWitness = addressTypesToMonitor.filter { $0 != addressType && Self.nativeWitnessTypes.contains($0) }
445456
return remainingNativeWitness.isEmpty
446457
}
447458

@@ -471,6 +482,7 @@ class SettingsViewModel: NSObject, ObservableObject {
471482

472483
do {
473484
try await lightningService.setPrimaryAddressType(addressType)
485+
syncMonitoredTypesFromNode()
474486
try await lightningService.sync()
475487
await generateAndUpdateAddress(addressType: addressType, wallet: wallet)
476488
} catch {
@@ -727,6 +739,39 @@ class SettingsViewModel: NSObject, ObservableObject {
727739
if let rgsServerUrl = dict["rgsServerUrl"] as? String, !rgsServerUrl.isEmpty {
728740
rgsConfigService.saveServerUrl(rgsServerUrl)
729741
}
742+
743+
syncAppStorageFromDefaults()
744+
745+
let restoredMonitored = addressTypesToMonitor
746+
let restoredPrimary = selectedAddressType
747+
restoredMonitoredTypesFromBackup = restoredMonitored.count > 1
748+
|| (restoredMonitored.count == 1 && restoredMonitored.first != restoredPrimary)
749+
750+
Logger.debug(
751+
"Restored settings: selectedAddressType=\(_selectedAddressType), addressTypesToMonitor=\(_addressTypesToMonitor), fromBackup=\(restoredMonitoredTypesFromBackup)",
752+
context: "SettingsRestore"
753+
)
754+
}
755+
756+
/// Re-read UserDefaults into @AppStorage properties after a direct defaults write.
757+
private func syncAppStorageFromDefaults() {
758+
_swipeBalanceToHide = defaults.object(forKey: "swipeBalanceToHide") as? Bool ?? true
759+
defaultTransactionSpeed = TransactionSpeed(rawValue: defaults.string(forKey: "defaultTransactionSpeed") ?? "") ?? .normal
760+
hideBalance = defaults.bool(forKey: "hideBalance")
761+
hideBalanceOnOpen = defaults.bool(forKey: "hideBalanceOnOpen")
762+
readClipboard = defaults.bool(forKey: "readClipboard")
763+
warnWhenSendingOver100 = defaults.bool(forKey: "warnWhenSendingOver100")
764+
enableQuickpay = defaults.bool(forKey: "enableQuickpay")
765+
quickpayAmount = defaults.double(forKey: "quickpayAmount")
766+
enableNotifications = defaults.bool(forKey: "enableNotifications")
767+
requirePinForPayments = defaults.bool(forKey: "requirePinForPayments")
768+
useBiometrics = defaults.bool(forKey: "useBiometrics")
769+
showWidgets = defaults.object(forKey: "showWidgets") as? Bool ?? true
770+
showWidgetTitles = defaults.bool(forKey: "showWidgetTitles")
771+
_coinSelectionMethod = defaults.string(forKey: "coinSelectionMethod") ?? CoinSelectionMethod.autopilot.rawValue
772+
_coinSelectionAlgorithm = defaults.string(forKey: "coinSelectionAlgorithm") ?? CoinSelectionAlgorithm.branchAndBound.stringValue
773+
_selectedAddressType = defaults.string(forKey: "selectedAddressType") ?? "nativeSegwit"
774+
_addressTypesToMonitor = defaults.string(forKey: "addressTypesToMonitor") ?? "nativeSegwit"
730775
}
731776

732777
/// Gets the current app cache data for backup

Bitkit/Views/Onboarding/RestoreWalletView.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,12 +258,16 @@ struct RestoreWalletView: View {
258258
wallet.isRestoringWallet = true
259259
app.showAllEmptyStates(false)
260260

261+
// Prevent settings changes from triggering backups before the actual restore runs
262+
BackupService.shared.setRestoring(true)
263+
261264
// When restoring a wallet, monitor all address types to catch any existing funds
262265
SettingsViewModel.shared.monitorAllAddressTypes()
263266

264267
_ = try StartupHandler.restoreWallet(mnemonic: bip39Mnemonic, bip39Passphrase: bip39Passphrase)
265268
try wallet.setWalletExistsState()
266269
} catch {
270+
BackupService.shared.setRestoring(false)
267271
app.toast(error)
268272
}
269273
}

Bitkit/Views/Onboarding/WalletRestoreSuccess.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,11 @@ struct WalletRestoreSuccess: View {
3636
app.backupVerified = true
3737
wallet.isRestoringWallet = false
3838

39-
// Prune empty address types on next syncCompleted
40-
SettingsViewModel.shared.pendingRestoreAddressTypePrune = true
39+
// Skip pruning if backup had explicit monitored address types
40+
let settings = SettingsViewModel.shared
41+
if !settings.restoredMonitoredTypesFromBackup {
42+
settings.pendingRestoreAddressTypePrune = true
43+
}
4144
}
4245
.accessibilityIdentifier("GetStartedButton")
4346
}

Bitkit/Views/Settings/Advanced/AddressTypePreferenceView.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ struct AddressTypePreferenceView: View {
105105
addressType: addressType,
106106
isSelected: settingsViewModel.selectedAddressType == addressType
107107
) {
108+
guard !settingsViewModel.isChangingAddressType else { return }
108109
guard settingsViewModel.selectedAddressType != addressType else { return }
109110

110111
app.toast(
@@ -138,6 +139,7 @@ struct AddressTypePreferenceView: View {
138139
}
139140
}
140141
}
142+
.disabled(settingsViewModel.isChangingAddressType)
141143
}
142144

143145
if showDevSettings {
@@ -168,6 +170,8 @@ struct AddressTypePreferenceView: View {
168170
isMonitored: settingsViewModel.isMonitoring(addressType),
169171
isSelectedType: settingsViewModel.selectedAddressType == addressType
170172
) { enabled in
173+
guard !settingsViewModel.isChangingAddressType else { return }
174+
171175
app.toast(type: .info, title: t("settings__adv__addr_type_applying"), autoHide: false)
172176

173177
Task {
@@ -207,6 +211,7 @@ struct AddressTypePreferenceView: View {
207211
}
208212
}
209213
}
214+
.disabled(settingsViewModel.isChangingAddressType)
210215
}
211216
}
212217

BitkitTests/AddressTypeSettingsTests.swift

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Combine
12
import LDKNode
23
import XCTest
34

@@ -199,4 +200,80 @@ final class AddressTypeSettingsTests: XCTestCase {
199200
let monitored = SettingsViewModel.parseAddressTypesString(raw ?? "")
200201
XCTAssertEqual(monitored, [.nativeSegwit, .taproot], "Invalid types should be filtered when parsing")
201202
}
203+
204+
// MARK: - KVO / Publisher notification
205+
206+
func testSettingsPublisherFiresOnAddressTypeChange() {
207+
let expectation = XCTestExpectation(description: "settingsPublisher should fire when selectedAddressType changes")
208+
var receivedDict: [String: Any]?
209+
210+
settings.selectedAddressType = .nativeSegwit
211+
UserDefaults.standard.synchronize()
212+
213+
let cancellable = settings.settingsPublisher
214+
.first()
215+
.sink { dict in
216+
receivedDict = dict
217+
expectation.fulfill()
218+
}
219+
220+
settings.selectedAddressType = .taproot
221+
222+
wait(for: [expectation], timeout: 2.0)
223+
224+
XCTAssertEqual(receivedDict?["selectedAddressType"] as? String, "taproot",
225+
"Publisher should contain updated selectedAddressType")
226+
cancellable.cancel()
227+
}
228+
229+
func testSettingsPublisherFiresOnMonitoredTypesChange() {
230+
let expectation = XCTestExpectation(description: "settingsPublisher should fire when addressTypesToMonitor changes")
231+
var receivedDict: [String: Any]?
232+
233+
settings.addressTypesToMonitor = [.nativeSegwit]
234+
UserDefaults.standard.synchronize()
235+
236+
let cancellable = settings.settingsPublisher
237+
.first()
238+
.sink { dict in
239+
receivedDict = dict
240+
expectation.fulfill()
241+
}
242+
243+
settings.addressTypesToMonitor = [.nativeSegwit, .taproot]
244+
245+
wait(for: [expectation], timeout: 2.0)
246+
247+
XCTAssertEqual(receivedDict?["addressTypesToMonitor"] as? String, "nativeSegwit,taproot",
248+
"Publisher should contain updated addressTypesToMonitor")
249+
cancellable.cancel()
250+
}
251+
252+
func testFullBackupRestoreRoundTrip() {
253+
settings.selectedAddressType = .taproot
254+
settings.addressTypesToMonitor = [.nativeSegwit, .taproot, .legacy]
255+
settings.hideBalance = true
256+
settings.enableQuickpay = true
257+
UserDefaults.standard.synchronize()
258+
259+
let backupDict = settings.getSettingsDictionary()
260+
261+
settings.resetToDefaults()
262+
UserDefaults.standard.synchronize()
263+
264+
XCTAssertEqual(settings.selectedAddressType, .nativeSegwit, "Should be default after reset")
265+
XCTAssertEqual(settings.addressTypesToMonitor, [.nativeSegwit], "Should be default after reset")
266+
267+
settings.restoreSettingsDictionary(backupDict)
268+
UserDefaults.standard.synchronize()
269+
270+
XCTAssertEqual(settings.selectedAddressType, .taproot,
271+
"selectedAddressType should survive full backup→reset→restore cycle")
272+
XCTAssertEqual(settings.addressTypesToMonitor, [.nativeSegwit, .taproot, .legacy],
273+
"addressTypesToMonitor should survive full backup→reset→restore cycle")
274+
XCTAssertEqual(settings.hideBalance, true,
275+
"hideBalance should survive full backup→reset→restore cycle")
276+
XCTAssertEqual(settings.enableQuickpay, true,
277+
"enableQuickpay should survive full backup→reset→restore cycle")
278+
}
202279
}

0 commit comments

Comments
 (0)