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
32 changes: 32 additions & 0 deletions DashWallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,14 @@
5AEDE0012F9B000000000004 /* EvonodeWithdrawalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEDE0012F9B000000000001 /* EvonodeWithdrawalViewModel.swift */; };
5AEDE0012F9B000000000005 /* EvonodeWithdrawalScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEDE0012F9B000000000002 /* EvonodeWithdrawalScreen.swift */; };
5AEDE0012F9B000000000006 /* EvonodeWithdrawalScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEDE0012F9B000000000002 /* EvonodeWithdrawalScreen.swift */; };
5AFDE1012FA1000000000011 /* TrackedMasternodeKeyVault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AFDE1012FA1000000000001 /* TrackedMasternodeKeyVault.swift */; };
5AFDE1012FA1000000000012 /* TrackedMasternodeKeyVault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AFDE1012FA1000000000001 /* TrackedMasternodeKeyVault.swift */; };
5AFDE1012FA1000000000013 /* AddMasternodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AFDE1012FA1000000000002 /* AddMasternodeViewModel.swift */; };
5AFDE1012FA1000000000014 /* AddMasternodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AFDE1012FA1000000000002 /* AddMasternodeViewModel.swift */; };
5AFDE1012FA1000000000015 /* AddMasternodeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AFDE1012FA1000000000003 /* AddMasternodeScreen.swift */; };
5AFDE1012FA1000000000016 /* AddMasternodeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AFDE1012FA1000000000003 /* AddMasternodeScreen.swift */; };
5AFDE1012FA1000000000017 /* TrackedMasternodeDetailScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AFDE1012FA1000000000004 /* TrackedMasternodeDetailScreen.swift */; };
5AFDE1012FA1000000000018 /* TrackedMasternodeDetailScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AFDE1012FA1000000000004 /* TrackedMasternodeDetailScreen.swift */; };
5AE5700E2F9C000000000003 /* EvonodeStatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AE5700E2F9C000000000001 /* EvonodeStatusViewModel.swift */; };
5AE5700E2F9C000000000004 /* EvonodeStatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AE5700E2F9C000000000001 /* EvonodeStatusViewModel.swift */; };
5AE5700E2F9C000000000005 /* EvonodeStatusScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AE5700E2F9C000000000002 /* EvonodeStatusScreen.swift */; };
Expand Down Expand Up @@ -2819,6 +2827,10 @@
51AA00202F970020005A0020 /* MasternodesScreen.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MasternodesScreen.swift; sourceTree = "<group>"; };
5AEDE0012F9B000000000001 /* EvonodeWithdrawalViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EvonodeWithdrawalViewModel.swift; sourceTree = "<group>"; };
5AEDE0012F9B000000000002 /* EvonodeWithdrawalScreen.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EvonodeWithdrawalScreen.swift; sourceTree = "<group>"; };
5AFDE1012FA1000000000001 /* TrackedMasternodeKeyVault.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrackedMasternodeKeyVault.swift; sourceTree = "<group>"; };
5AFDE1012FA1000000000002 /* AddMasternodeViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddMasternodeViewModel.swift; sourceTree = "<group>"; };
5AFDE1012FA1000000000003 /* AddMasternodeScreen.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddMasternodeScreen.swift; sourceTree = "<group>"; };
5AFDE1012FA1000000000004 /* TrackedMasternodeDetailScreen.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrackedMasternodeDetailScreen.swift; sourceTree = "<group>"; };
5AE5700E2F9C000000000001 /* EvonodeStatusViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EvonodeStatusViewModel.swift; sourceTree = "<group>"; };
5AE5700E2F9C000000000002 /* EvonodeStatusScreen.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EvonodeStatusScreen.swift; sourceTree = "<group>"; };
51BA2E9F2F0B2E0100A1B201 /* SwapPendingGate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapPendingGate.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3776,6 +3788,7 @@
5AEEB0012F9C000000000001 /* EvonodeEpochBlocksService.swift */,
5AEEB0012F9C000000000005 /* EvonodeEpochBlocksMonitor.swift */,
5AEEB0012F9C00000000000B /* EvonodeProposalActivity.swift */,
5AFDE1012FA1000000000001 /* TrackedMasternodeKeyVault.swift */,
);
path = Masternodes;
sourceTree = "<group>";
Expand All @@ -3789,6 +3802,16 @@
path = "Masternode Withdrawal";
sourceTree = "<group>";
};
5AFDE1012FA1000000000005 /* Tracked Masternodes */ = {
isa = PBXGroup;
children = (
5AFDE1012FA1000000000002 /* AddMasternodeViewModel.swift */,
5AFDE1012FA1000000000003 /* AddMasternodeScreen.swift */,
5AFDE1012FA1000000000004 /* TrackedMasternodeDetailScreen.swift */,
);
path = "Tracked Masternodes";
sourceTree = "<group>";
};
5AE5700E2F9C000000000007 /* Evonode Status */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -4841,6 +4864,7 @@
FB3FF0B0222641210059A9A5 /* Masternode Keys */,
51AA00202F970020005A0020 /* MasternodesScreen.swift */,
5AEDE0012F9B000000000007 /* Masternode Withdrawal */,
5AFDE1012FA1000000000005 /* Tracked Masternodes */,
5AE5700E2F9C000000000007 /* Evonode Status */,
7566F4822BB69498005238D2 /* ToolsMenuScreen.swift */,
RC00C0032FA0000000000001 /* CSVExportSheet.swift */,
Expand Down Expand Up @@ -10224,6 +10248,10 @@
51AA00222F970022005A0022 /* MasternodesScreen.swift in Sources */,
5AEDE0012F9B000000000004 /* EvonodeWithdrawalViewModel.swift in Sources */,
5AEDE0012F9B000000000006 /* EvonodeWithdrawalScreen.swift in Sources */,
5AFDE1012FA1000000000012 /* TrackedMasternodeKeyVault.swift in Sources */,
5AFDE1012FA1000000000014 /* AddMasternodeViewModel.swift in Sources */,
5AFDE1012FA1000000000016 /* AddMasternodeScreen.swift in Sources */,
5AFDE1012FA1000000000018 /* TrackedMasternodeDetailScreen.swift in Sources */,
5AE5700E2F9C000000000004 /* EvonodeStatusViewModel.swift in Sources */,
5AE5700E2F9C000000000006 /* EvonodeStatusScreen.swift in Sources */,
51AA00022F970002005A0002 /* SyncInfoMenuScreen.swift in Sources */,
Expand Down Expand Up @@ -11240,6 +11268,10 @@
51AA00212F970021005A0021 /* MasternodesScreen.swift in Sources */,
5AEDE0012F9B000000000003 /* EvonodeWithdrawalViewModel.swift in Sources */,
5AEDE0012F9B000000000005 /* EvonodeWithdrawalScreen.swift in Sources */,
5AFDE1012FA1000000000011 /* TrackedMasternodeKeyVault.swift in Sources */,
5AFDE1012FA1000000000013 /* AddMasternodeViewModel.swift in Sources */,
5AFDE1012FA1000000000015 /* AddMasternodeScreen.swift in Sources */,
5AFDE1012FA1000000000017 /* TrackedMasternodeDetailScreen.swift in Sources */,
5AE5700E2F9C000000000003 /* EvonodeStatusViewModel.swift in Sources */,
5AE5700E2F9C000000000005 /* EvonodeStatusScreen.swift in Sources */,
51AA00032F970003005A0003 /* SyncInfoMenuScreen.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,14 @@ final class EvonodeEpochBlocksMonitor: ObservableObject {
static func activeEvonodeProTxHashes() -> Set<Data> {
guard let manager = SwiftDashSDKHost.shared.manager,
let walletId = SwiftDashSDKHost.shared.wallet?.walletId else { return [] }
return Set(manager.masternodes(for: walletId)
.filter { $0.isEvonode && MasternodeStatus(rawValue: $0.status) != .retired }
.map(\.proTxHash))
let stillListed: (PlatformMasternode) -> Bool = {
$0.isEvonode && MasternodeStatus(rawValue: $0.status) != .retired
}
// Wallet evonodes plus the user's tracked ones — the epoch-blocks
// range scan is privacy-preserving either way (it never names the
// nodes), so tracked evonodes ride the same tally.
return Set(manager.masternodes(for: walletId).filter(stillListed).map(\.proTxHash))
.union(manager.trackedMasternodes().filter(stillListed).map(\.proTxHash))
}

// MARK: Triggers
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
//
// Created by Claude Code
// Copyright © 2026 Dash Core Group. All rights reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation
import Security
import SwiftData
import SwiftDashSDK

// MARK: - TrackedMasternodeKeyVaulting

/// Secure storage for the private keys a user attaches to a TRACKED
/// (wallet-independent) masternode.
///
/// The SDK deliberately never stores these: Rust receives a key per signing
/// call and retains nothing (`trackedMasternodeWithdraw`,
/// `castContestedResourceVote`). The app owns them, in the keychain, keyed by
/// `(network, proTxHash, role)`. Values are the key TEXT exactly as the user
/// supplied it (WIF / hex / node-key base64) — kept verbatim so it can be
/// re-shown and re-parsed by the same SDK parser that validated it.
///
/// Callers gate every read behind `AuthenticationGate` — this type performs
/// no prompting of its own. Main-actor-bound like the SDK `KeychainManager`
/// APIs it wraps (and every caller already is).
@MainActor
protocol TrackedMasternodeKeyVaulting {
/// The stored key text for a role, or `nil`.
func key(for proTxHash: Data, role: MasternodeKeyRole) -> String?
/// Store (or replace) a role's key text. Returns `false` on a keychain
/// write failure — callers surface that, never assume success.
@discardableResult
func store(_ keyText: String, for proTxHash: Data, role: MasternodeKeyRole) -> Bool
/// Remove one role's key.
@discardableResult
func removeKey(for proTxHash: Data, role: MasternodeKeyRole) -> Bool
/// The roles that currently have a key attached for this node.
func attachedRoles(for proTxHash: Data) -> Set<MasternodeKeyRole>
/// Remove every key of this node (untrack).
func removeAllKeys(for proTxHash: Data)
}

// MARK: - TrackedMasternodeKeyVault

/// Keychain-backed vault (SwiftDashSDK `KeychainManager`, the same unified
/// service the SDK's identity keys use). Entries are per network so a node
/// tracked on both networks keeps distinct keys.
final class TrackedMasternodeKeyVault: TrackedMasternodeKeyVaulting {
private let keychain: KeychainManager
/// Resolved per call — the vault outlives network switches. Defaults to
/// the persisted network selection (`WalletEnvironment`), which is
/// readable off the main actor, unlike the host's published state.
private let network: () -> Network?

nonisolated init(keychain: KeychainManager = .shared,
network: @escaping () -> Network? = {
WalletEnvironment.isTestnet ? .testnet : .mainnet
}) {
self.keychain = keychain
self.network = network
}

/// The roles the tracked-masternode UI manages. `platformNode` and
/// `operatorPayout` are deliberately absent — no app action uses them
/// (owner decision 2026-08-24).
static let managedRoles: [MasternodeKeyRole] = [.owner, .voting, .operator, .ownerPayout]

private func identifier(_ proTxHash: Data, _ role: MasternodeKeyRole) -> String? {
guard let network = network() else { return nil }
let hex = proTxHash.map { String(format: "%02x", $0) }.joined()
return "masternode.\(network.rawValue).\(hex).\(role.rawValue)"
}

func key(for proTxHash: Data, role: MasternodeKeyRole) -> String? {
guard let identifier = identifier(proTxHash, role),
let data = keychain.retrieveKeyData(identifier: identifier) else { return nil }
return String(data: data, encoding: .utf8)
}

@discardableResult
func store(_ keyText: String, for proTxHash: Data, role: MasternodeKeyRole) -> Bool {
guard let identifier = identifier(proTxHash, role) else { return false }
return keychain.storeKeyData(Data(keyText.utf8), identifier: identifier) != nil
}

@discardableResult
func removeKey(for proTxHash: Data, role: MasternodeKeyRole) -> Bool {
guard let identifier = identifier(proTxHash, role) else { return false }
return keychain.deleteKeyData(identifier: identifier)
}

func attachedRoles(for proTxHash: Data) -> Set<MasternodeKeyRole> {
Set(Self.managedRoles.filter { role in
guard let identifier = identifier(proTxHash, role) else { return false }
return keychain.retrieveKeyData(identifier: identifier) != nil
})
}

func removeAllKeys(for proTxHash: Data) {
for role in Self.managedRoles {
removeKey(for: proTxHash, role: role)
}
}
}


// MARK: - Reset-all cleanup

extension TrackedMasternodeKeyVault {
/// Reset-all teardown (owner decision 2026-08-24: tracked masternodes
/// survive deleting one wallet of several, die on reset-all): untrack
/// everything the running manager knows (clears the Rust registry and
/// the current network's persisted rows), sweep any remaining
/// `PersistentTrackedMasternode` rows (the other network), and delete
/// every vaulted key. Idempotent and best-effort — a failed step logs
/// and the rest still runs.
@MainActor
static func wipeAllTrackedState() {
if let manager = SwiftDashSDKHost.shared.manager {
for node in manager.trackedMasternodes() {
_ = try? manager.untrackMasternode(proTxHash: node.proTxHash)
}
}
if let container = SwiftDashSDKHost.shared.modelContainer {
let context = ModelContext(container)
try? context.delete(model: PersistentTrackedMasternode.self)
try? context.save()
}
removeAllVaultedKeys()
}

/// Delete every `masternode.*` entry in the unified keychain service,
/// across networks and nodes.
@MainActor
static func removeAllVaultedKeys() {
let service = KeychainManager.shared.serviceName
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecMatchLimit as String: kSecMatchLimitAll,
kSecReturnAttributes as String: true,
]
var result: AnyObject?
guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess,
let items = result as? [[String: Any]] else { return }
for item in items {
guard let account = item[kSecAttrAccount as String] as? String,
account.hasPrefix("masternode.") else { continue }
let deleteQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
]
SecItemDelete(deleteQuery as CFDictionary)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,19 @@ final class SwiftDashSDKWalletWiper: NSObject {
logger.info(
"wiped SwiftDashSDK wallets across mainnet/testnet in \(String(describing: elapsed), privacy: .public); authorization=\(authorization.logLabel, privacy: .public)")

// Reset-all also drops the user's TRACKED (wallet-independent)
// masternodes and their vaulted keys — they survive single-wallet
// deletion, not a full reset (owner decision 2026-08-24). Runs
// SYNCHRONOUSLY before the runtime teardown below: the cleanup
// needs the host's manager and model container, which
// `handleWalletWiped()` tears down. This body runs on the wipe
// executor's background queue, so the main hop cannot deadlock.
DispatchQueue.main.sync {
MainActor.assumeIsolated {
TrackedMasternodeKeyVault.wipeAllTrackedState()
}
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Tear down the app-owned runtime now that all wallet material is
// gone. This stops BLAST/SPV, drops the host-owned manager/wallet, and
// clears published wallet state. We do NOT delete public chain data;
Expand Down
Loading
Loading