Skip to content
Merged
Show file tree
Hide file tree
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
33 changes: 33 additions & 0 deletions DashWallet/Sources/Models/CrowdNode/CrowdNode+UserDefaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ private let kOnlineInfoShown = "crowdNodeOnlineInfoShownKey"
private let kSignedEmailMessageId = "crowdNodeSignedEmailMessageId"
private let kShouldShowConfirmedNotification = "shouldShowConfirmedNotification"
private let kLastWithdrawalBlock = "lastWithdrawalBlockKey"
// Unlike the keys above this one is NOT legacy — it was introduced after the
// per-wallet scoping, so no pre-multi-wallet install ever wrote the bare key.
// It still routes through `perWalletKey(_:)` like the rest: the bare name is
// only the no-active-wallet fallback.
private let kFruitlessRestoreTxCount = "crowdNodeFruitlessRestoreTxCountKey"

/// The set of legacy keys that are scoped per wallet. Enumerated by `resetForWipe`
/// (which must clear the per-wallet variant for EVERY wallet) and used by
Expand All @@ -58,6 +63,7 @@ private let kPerWalletKeys = [
kSignedEmailMessageId,
kShouldShowConfirmedNotification,
kLastWithdrawalBlock,
kFruitlessRestoreTxCount,
]

// MARK: - CrowdNodeDefaults
Expand Down Expand Up @@ -124,6 +130,7 @@ class CrowdNodeDefaults {
_shouldShowConfirmedNotification = nil
_signedEmailMessageId = nil
_lastWithdrawalBlock = nil
_fruitlessRestoreTxCount = nil
}

private var _accountAddress: String? = nil
Expand Down Expand Up @@ -261,6 +268,31 @@ class CrowdNodeDefaults {
}
}

private var _fruitlessRestoreTxCount: Int? = nil
/// Persisted-row count as of the last `restoreState()` pass that scanned
/// this wallet's full post-2022 history and found no CrowdNode account at
/// all, or nil when no such pass is on record. Lets a later launch skip
/// the full history scan when nothing was persisted since — the memo is
/// keyed to the row count rather than latching permanently precisely so a
/// restored seed whose history syncs in later (count changes) rescans.
/// Written ONLY by the account-not-found branch; setting nil clears it.
///
/// Resolution deliberately bypasses `resolvedKey`'s seed-from-legacy step:
/// the bare key can only hold a memo written while no wallet was active,
/// i.e. from a pass whose wallet-scoped scan was vacuous — seeding it into
/// a wallet's key would let that wallet skip a scan it never ran.
var fruitlessRestoreTxCount: Int? {
get {
let key = perWalletKey(kFruitlessRestoreTxCount) ?? kFruitlessRestoreTxCount
return _fruitlessRestoreTxCount ?? UserDefaults.standard.value(forKey: key) as? Int
}
set(value) {
_fruitlessRestoreTxCount = value
let key = perWalletKey(kFruitlessRestoreTxCount) ?? kFruitlessRestoreTxCount
UserDefaults.standard.set(value, forKey: key)
}
}


/// Reset the ACTIVE wallet's CrowdNode state (plus the two global education
/// flags). Writes through the per-wallet-scoped properties, so it clears the
Expand All @@ -278,6 +310,7 @@ class CrowdNodeDefaults {
onlineInfoShown = false
signedEmailMessageId = -1
lastWithdrawalBlock = 0
fruitlessRestoreTxCount = nil
}

/// Clear a SINGLE wallet's per-wallet CrowdNode keys (the removed wallet may
Expand Down
49 changes: 28 additions & 21 deletions DashWallet/Sources/Models/CrowdNode/CrowdNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,18 +124,6 @@ public final class CrowdNode {
private(set) var isOnlineStateRestored = false
var showNotificationOnResult = false

/// Persisted transaction count as of the last `restoreState()` that found
/// no CrowdNode account at all.
///
/// The restore's own guard is `signUpState > .notStarted`, which a wallet
/// that never signed up never reaches — so every caller (launch sync-done,
/// each entry into the CrowdNode portal) re-ran the full history scan and
/// blocked the main thread for seconds apiece. A signup can still turn up
/// later when a restored seed syncs its history in, so this memo is keyed
/// to the store's row count rather than latching permanently: new rows
/// persisted means the scan gets to run again.
private var fruitlessRestoreTxCount: Int?

var masternodeAPY: Double
var crowdnodeAPY: Double

Expand Down Expand Up @@ -213,9 +201,18 @@ extension CrowdNode {
// a scan that still owes work on those rows.
let txCountBeforeScans = TransactionObserver.persistedTransactionCount()

// A previous pass already scanned this exact history and found no
// account; without new rows it would reach the same conclusion.
if let scanned = fruitlessRestoreTxCount, txCountBeforeScans == scanned {
// A previous pass — this launch or an earlier one; the memo persists
// per wallet in CrowdNodeDefaults — already scanned this exact history
// and found no account; without new rows it would reach the same
// conclusion. The restore's own guard is `signUpState > .notStarted`,
// which a wallet that never signed up never reaches, so without this
// memo every caller (launch sync-done, each entry into the CrowdNode
// portal) re-runs the full history scan and blocks the main thread
// for seconds apiece. A signup can still turn up later when a
// restored seed syncs its history in, which is why the memo is keyed
// to the row count rather than latching: new rows persisted → miss →
// the scan runs again.
if let scanned = prefs.fruitlessRestoreTxCount, txCountBeforeScans == scanned {
return
}

Expand Down Expand Up @@ -258,8 +255,13 @@ extension CrowdNode {
DWLogger.log("CrowdNode: account not found")
// Nothing found by either the signup scan or the online-account
// lookup, so this whole pass was a no-op — memoize it against the
// history the scans actually saw, not the store's count now.
fruitlessRestoreTxCount = txCountBeforeScans
// history the scans actually saw, not the store's count now. A nil
// count means the SDK container wasn't up and the scans were
// vacuous: nothing to memoize, and don't clobber a prior launch's
// valid memo with it.
if let txCountBeforeScans {
prefs.fruitlessRestoreTxCount = txCountBeforeScans
}
}
}

Expand Down Expand Up @@ -359,7 +361,8 @@ extension CrowdNode {
primaryAddress = nil
apiError = nil
balance = 0
fruitlessRestoreTxCount = nil
// resetUserDefaults() also clears the persisted fruitless-restore memo,
// so a network change or alien-address teardown always rescans.
prefs.resetUserDefaults()
}

Expand Down Expand Up @@ -393,9 +396,13 @@ extension CrowdNode {
apiError = nil
balance = 0
isOnlineStateRestored = false
// The memo describes the PREVIOUS wallet's scan; the new wallet must
// get a real one even though the store's row count is unchanged.
fruitlessRestoreTxCount = nil
// The active wallet has already changed here (and invalidateCache()
// above dropped the stale per-wallet resolution), so this clears the
// NEW wallet's persisted memo: the row count is store-global while the
// scan is wallet-scoped, so a memo recorded against a different
// wallet-mix of the store can't be trusted after a switch — force a
// real scan for this wallet.
prefs.fruitlessRestoreTxCount = nil
restoreState()
}

Expand Down
Loading