From 0c4735822f458b66972e6f9cc3c4c0e591af792d Mon Sep 17 00:00:00 2001 From: Borja Castellano Date: Fri, 6 Feb 2026 12:49:52 -0600 Subject: [PATCH 01/15] fixed the performance issue while sync --- .../Core/Services/WalletService.swift | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift index 028dfe10dfc..03bab1d45ab 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift @@ -554,18 +554,7 @@ public class WalletService: ObservableObject { SDKLogger.log("Sync started for manager: \(manager)", minimumLevel: .medium) } - func onComplete(_ headerTip: UInt32, _ cycle: UInt32) { - Task { @MainActor in - SDKLogger.log("Sync completed, header tip: \(headerTip), cycle: \(cycle)", minimumLevel: .medium) - - if let wm = walletService.walletManager { - for wallet in wm.wallets { - await wm.syncWalletStateFromRust(for: wallet) - } - } - } - } - + func onComplete(_ headerTip: UInt32, _ cycle: UInt32) {} func onBlockHeadersStored(_ tipHeight: UInt32) {} func onBlockHeadersSyncCompleted(_ tipHeight: UInt32) {} func onFilterHeadersStored(_ startHeight: UInt32, _ endHeight: UInt32, _ tipHeight: UInt32) {} @@ -623,6 +612,7 @@ public class WalletService: ObservableObject { _ txid: Data, _ amount: Int64, _ addresses: [String] +<<<<<<< HEAD ) { Task { @MainActor in if let wm = walletService.walletManager { @@ -637,6 +627,10 @@ public class WalletService: ObservableObject { } } +======= + ) { } + +>>>>>>> a72969687 (fixed the performance issue while sync) func onBalanceUpdated( _ walletId: String, _ spendable: UInt64, From f469b9d5be9742d745be3cbae7d452afc12ab73c Mon Sep 17 00:00:00 2001 From: Borja Castellano Date: Fri, 6 Feb 2026 13:17:23 -0600 Subject: [PATCH 02/15] Fitler match related views and code dropped from swift sdk --- .../Core/Models/FilterMatch.swift | 173 -------- .../Core/Services/FilterMatchService.swift | 273 ------------ .../Core/Views/FilterMatchesView.swift | 412 ------------------ 3 files changed, 858 deletions(-) delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Core/Models/FilterMatch.swift delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/FilterMatchService.swift delete mode 100644 packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/FilterMatchesView.swift diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Models/FilterMatch.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Models/FilterMatch.swift deleted file mode 100644 index fa847e70e9b..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Models/FilterMatch.swift +++ /dev/null @@ -1,173 +0,0 @@ -// -// FilterMatch.swift -// SwiftDashSDK -// -// Models for compact filters from SPV client -// - -import Foundation -import DashSDKFFI - -/// A single compact filter with its height and data -public struct CompactFilter: Identifiable { - public var id: UInt32 { height } - - /// Block height for this filter - public let height: UInt32 - - /// Filter data bytes - public let data: Data - - public init(height: UInt32, data: Data) { - self.height = height - self.data = data - } - - // NOTE: FFI initializer commented out - FFICompactFilter not available in current FFI - // /// Initialize from FFI struct - // public init(from ffiFilter: FFICompactFilter) { - // self.height = ffiFilter.height - // - // if let dataPtr = ffiFilter.data, ffiFilter.data_len > 0 { - // self.data = Data(bytes: dataPtr, count: Int(ffiFilter.data_len)) - // } else { - // self.data = Data() - // } - // } - - /// Get filter size in bytes - public var sizeInBytes: Int { - data.count - } -} - -/// Collection of compact filters -public struct CompactFilters { - public let filters: [CompactFilter] - - public init(filters: [CompactFilter]) { - self.filters = filters - } - - // NOTE: FFI initializer commented out - FFICompactFilters not available in current FFI - // /// Initialize from FFI struct - // public init(from ffiFilters: FFICompactFilters) { - // var filters: [CompactFilter] = [] - // - // if let filtersPtr = ffiFilters.filters { - // for i in 0.. = [] - - /// Loading state - @Published public private(set) var isLoading = false - - /// Error state - @Published public private(set) var error: FilterMatchError? - - /// Total height range available - @Published public private(set) var heightRange: ClosedRange? - - // MARK: - Private Properties - - /// Reference to wallet service for SPV client access - private weak var walletService: WalletService? - - /// Batch size for loading (must be ≤ 10,000 per FFI constraint) - private let batchSize: UInt32 = 1_000 - - /// Pre-fetch threshold (load more when this many rows from end) - private let prefetchThreshold: Int = 50 - - /// Cache of loaded height ranges to avoid duplicate queries - private var loadedRanges: [ClosedRange] = [] - - // MARK: - Initialization - - public init(walletService: WalletService) { - self.walletService = walletService - } - - // MARK: - Computed Properties - - /// Filters that matched wallet addresses - public var matchedFilters: [CompactFilter] { - filters.filter { matchedHeights.contains($0.height) } - } - - /// Check if a specific height has a matched filter - public func isFilterMatched(_ height: UInt32) -> Bool { - matchedHeights.contains(height) - } - - // MARK: - Public Methods - - /// Initialize the service and load the initial batch - public func initialize(endHeight: UInt32) async { - print("🔍 FilterMatchService: Initializing with endHeight=\(endHeight)") - self.heightRange = 0...endHeight - await loadMatchedHeights() - await loadInitialBatch() - } - - /// Update the height range (when sync progresses) - public func updateHeightRange(endHeight: UInt32) { - self.heightRange = 0...endHeight - } - - /// Jump to a specific height and load surrounding data - public func jumpTo(height: UInt32) async { - guard let range = heightRange, - range.contains(height) else { - error = .invalidRange("Height \(height) is outside valid range") - return - } - - // Clear existing filters and load around the target height - filters = [] - loadedRanges = [] - - // Load batch containing the target height, avoiding underflow - let startHeight: UInt32 - if height >= batchSize / 2 { - startHeight = height - batchSize / 2 - } else { - startHeight = range.lowerBound - } - await loadBatch(startHeight: startHeight) - } - - /// Check if we need to prefetch more data based on scroll position - public func checkPrefetch(displayedIndex: Int) async { - guard !isLoading, - let range = heightRange else { - return - } - - // Check if we're near the end of loaded data - if displayedIndex >= filters.count - prefetchThreshold { - // Load more data before the oldest loaded height - if let oldestLoaded = filters.last?.height, - oldestLoaded > range.lowerBound { - // Avoid underflow when calculating startHeight - let startHeight: UInt32 - if oldestLoaded >= batchSize { - startHeight = max(range.lowerBound, oldestLoaded - batchSize) - } else { - startHeight = range.lowerBound - } - await loadBatch(startHeight: startHeight) - } - } - - // Check if we're near the beginning and need newer data - if displayedIndex < prefetchThreshold { - if let newestLoaded = filters.first?.height, - newestLoaded < range.upperBound { - let startHeight = min(range.upperBound - batchSize + 1, newestLoaded + 1) - await loadBatch(startHeight: startHeight) - } - } - } - - /// Reload all data (useful after sync completes) - public func reload() async { - filters = [] - loadedRanges = [] - await loadInitialBatch() - } - - // MARK: - Private Methods - - /// Load matched filter heights from FFI - /// NOTE: FFI functions for filter matching are not yet available in current FFI - private func loadMatchedHeights() async { - guard walletService != nil, - heightRange != nil else { - print("❌ FilterMatchService: Cannot load matched heights - client not available") - return - } - - print("🔍 FilterMatchService: Filter matching FFI not available in current build") - - // NOTE: The following FFI functions are not available in the current FFI: - // - dash_spv_ffi_client_get_filter_matched_heights - // - dash_spv_ffi_filter_matches_destroy - // When these become available, uncomment and use: - // - // guard let client = walletService.spvClientHandle else { return } - // let matchesPtr = dash_spv_ffi_client_get_filter_matched_heights(client, range.lowerBound, range.upperBound + 1) - // defer { if let ptr = matchesPtr { dash_spv_ffi_filter_matches_destroy(ptr) } } - // guard let ptr = matchesPtr else { return } - // let ffiMatches = ptr.pointee - // let filterMatches = FilterMatches(from: ffiMatches) - // var heights = Set() - // for entry in filterMatches.entries { heights.insert(entry.height) } - // matchedHeights = heights - - matchedHeights = Set() - } - - private func loadInitialBatch() async { - guard let range = heightRange else { return } - - // Load the most recent batch, avoiding underflow - let startHeight: UInt32 - if range.upperBound >= batchSize - 1 { - startHeight = max(range.lowerBound, range.upperBound - batchSize + 1) - } else { - startHeight = range.lowerBound - } - await loadBatch(startHeight: startHeight) - } - - /// NOTE: FFI functions for loading compact filters are not yet available in current FFI - private func loadBatch(startHeight: UInt32) async { - guard walletService != nil else { - print("❌ FilterMatchService: WalletService not available") - error = .clientNotAvailable - return - } - - guard let range = heightRange else { - print("❌ FilterMatchService: Height range not set") - error = .clientNotAvailable - return - } - - // Calculate end height (exclusive, max batchSize) - let endHeight = min(range.upperBound + 1, startHeight + batchSize) - - print("🔍 FilterMatchService: Loading filters from \(startHeight) to \(endHeight)") - print("🔍 FilterMatchService: Compact filter loading FFI not available in current build") - - // Check if this range is already loaded - let requestedRange = startHeight...endHeight - if loadedRanges.contains(where: { $0.overlaps(requestedRange) }) { - return - } - - isLoading = true - error = nil - - // NOTE: The following FFI functions are not available in the current FFI: - // - dash_spv_ffi_client_load_filters - // - dash_spv_ffi_compact_filters_destroy - // When these become available, uncomment and use: - // - // guard let client = walletService.spvClientHandle else { - // error = .clientNotAvailable - // isLoading = false - // return - // } - // let filtersPtr = dash_spv_ffi_client_load_filters(client, startHeight, endHeight) - // defer { if let ptr = filtersPtr { dash_spv_ffi_compact_filters_destroy(ptr) } } - // guard let ptr = filtersPtr else { - // if let errorCStr = dash_spv_ffi_get_last_error() { - // error = .ffiError(String(cString: errorCStr)) - // } else { - // error = .unknown - // } - // isLoading = false - // return - // } - // let ffiFilters = ptr.pointee - // let compactFilters = CompactFilters(from: ffiFilters) - // var allFilters = filters + compactFilters.filters - // allFilters.sort { $0.height > $1.height } - // var seenHeights = Set() - // allFilters = allFilters.filter { filter in - // if seenHeights.contains(filter.height) { return false } - // seenHeights.insert(filter.height) - // return true - // } - // filters = allFilters - - // Currently return empty filters since FFI is not available - loadedRanges.append(startHeight...(endHeight - 1)) - print("🔍 FilterMatchService: Stubbed - no filters loaded (FFI not available)") - - isLoading = false - } -} - -// MARK: - CompactFilter Hashable Conformance - -extension CompactFilter: Hashable { - public static func == (lhs: CompactFilter, rhs: CompactFilter) -> Bool { - lhs.height == rhs.height - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(height) - } -} - -// MARK: - Helper Extensions - -extension Data { - /// Convert Data to hex string for display - public func hexEncodedString() -> String { - return map { String(format: "%02hhx", $0) }.joined() - } -} diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/FilterMatchesView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/FilterMatchesView.swift deleted file mode 100644 index dae748d138d..00000000000 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/FilterMatchesView.swift +++ /dev/null @@ -1,412 +0,0 @@ -// -// FilterMatchesView.swift -// SwiftExampleApp -// -// View for displaying compact filter matches with smooth scrolling and jump-to -// - -import SwiftUI -import SwiftDashSDK - -enum FilterDisplayMode: String, CaseIterable { - case all = "All Filters" - case matched = "Matched Filters" -} - -struct FilterMatchesView: View { - @EnvironmentObject var walletService: WalletService - @StateObject private var service: FilterMatchService - @State private var showJumpToAlert = false - @State private var jumpToHeight = "" - @State private var expandedHeights: Set = [] - @State private var displayMode: FilterDisplayMode = .all - - init(walletService: WalletService) { - _service = StateObject(wrappedValue: FilterMatchService(walletService: walletService)) - } - - // Computed property to filter based on selected mode - private var filteredFilters: [CompactFilter] { - switch displayMode { - case .all: - return service.filters - case .matched: - return service.matchedFilters - } - } - - // Wait for sync to complete before loading filters - private func waitForSyncToComplete() async { - // If sync is not running, return immediately - guard !walletService.syncProgress.state.isRunning() else { return } - - print("⏳ Waiting for sync to complete before loading filters...") - - // Poll until sync completes (check every 0.5 seconds) - while !walletService.syncProgress.state.isComplete() { - try? await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds - } - - // Give extra time for client to fully release locks - try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second - - print("✅ Sync completed, ready to load filters") - } - - var body: some View { - VStack(spacing: 0) { - // Mode selector - modeSelector - - // Jump-to bar - jumpToBar - - // Main content - if service.isLoading && service.filters.isEmpty { - loadingView - } else if let error = service.error { - errorView(error) - } else if filteredFilters.isEmpty { - emptyView - } else { - filtersList - } - } - .navigationTitle("Compact Filters") - .navigationBarTitleDisplayMode(.inline) - .task { - // Wait for sync to complete before loading filters - await waitForSyncToComplete() - - // Initialize with current sync height - let currentHeight = walletService.syncProgress.filters?.currentHeight ?? 0 - await service.initialize(endHeight: currentHeight) - } - .alert("Jump to Height", isPresented: $showJumpToAlert) { - TextField("Block Height", text: $jumpToHeight) - .keyboardType(.numberPad) - - Button("Cancel", role: .cancel) { - jumpToHeight = "" - } - - Button("Go") { - if let height = UInt32(jumpToHeight) { - Task { - await service.jumpTo(height: height) - } - } - jumpToHeight = "" - } - } message: { - if let range = service.heightRange { - Text("Enter a height between \(range.lowerBound) and \(range.upperBound)") - } else { - Text("Enter a block height") - } - } - } - - // MARK: - Mode Selector - - private var modeSelector: some View { - Picker("Display Mode", selection: $displayMode) { - ForEach(FilterDisplayMode.allCases, id: \.self) { mode in - Text(mode.rawValue).tag(mode) - } - } - .pickerStyle(.segmented) - .padding(.horizontal) - .padding(.vertical, 12) - .background(Color(.systemBackground)) - .overlay( - Rectangle() - .frame(height: 1) - .foregroundColor(Color(.separator)), - alignment: .bottom - ) - } - - // MARK: - Jump-to Bar - - private var jumpToBar: some View { - HStack(spacing: 12) { - Text(displayMode == .all ? "All Filters" : "Matched Filters") - .font(.headline) - .foregroundColor(.secondary) - - Spacer() - - if !filteredFilters.isEmpty { - Text("\(filteredFilters.count) filter\(filteredFilters.count == 1 ? "" : "s")") - .font(.caption) - .foregroundColor(.secondary) - } - - Button(action: { - showJumpToAlert = true - }) { - HStack(spacing: 4) { - Image(systemName: "location.magnifyingglass") - Text("Jump to") - } - .font(.caption) - .padding(.horizontal, 12) - .padding(.vertical, 6) - .background(Color.blue) - .foregroundColor(.white) - .cornerRadius(8) - } - - Button(action: { - Task { - await service.reload() - } - }) { - Image(systemName: "arrow.clockwise") - .font(.caption) - .padding(8) - .background(Color.gray.opacity(0.2)) - .cornerRadius(8) - } - } - .padding(.horizontal) - .padding(.vertical, 12) - .background(Color(.systemBackground)) - .overlay( - Rectangle() - .frame(height: 1) - .foregroundColor(Color(.separator)), - alignment: .bottom - ) - } - - // MARK: - Filters List - - private var filtersList: some View { - ScrollViewReader { proxy in - List { - ForEach(Array(filteredFilters.enumerated()), id: \.element.id) { index, filter in - FilterRow(filter: filter, isExpanded: expandedHeights.contains(filter.height), isMatched: displayMode == .matched || service.isFilterMatched(filter.height)) - .onTapGesture { - withAnimation { - if expandedHeights.contains(filter.height) { - expandedHeights.remove(filter.height) - } else { - expandedHeights.insert(filter.height) - } - } - } - .onAppear { - // Trigger prefetch when this row appears - // Only prefetch in "All" mode since matched filters are already loaded - if displayMode == .all { - Task { - await service.checkPrefetch(displayedIndex: index) - } - } - } - } - - // Loading indicator at bottom - if service.isLoading { - HStack { - Spacer() - ProgressView() - .padding() - Spacer() - } - } - } - .listStyle(.plain) - } - } - - // MARK: - State Views - - private var loadingView: some View { - VStack(spacing: 16) { - ProgressView() - .scaleEffect(1.5) - - if !walletService.syncProgress.state.isComplete() { - VStack(spacing: 8) { - Text("Waiting for sync to complete...") - .font(.headline) - .foregroundColor(.secondary) - - Text("Filters cannot be loaded while sync is in progress") - .font(.caption) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - .padding(.horizontal) - } - } else { - Text("Loading compact filters...") - .font(.headline) - .foregroundColor(.secondary) - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - - private func errorView(_ error: FilterMatchError) -> some View { - VStack(spacing: 16) { - Image(systemName: "exclamationmark.triangle") - .font(.system(size: 48)) - .foregroundColor(.red) - - Text("Error") - .font(.headline) - - Text(error.localizedDescription) - .font(.subheadline) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - .padding(.horizontal) - - Button("Retry") { - Task { - // Wait for sync to complete before retrying - await waitForSyncToComplete() - await service.reload() - } - } - .buttonStyle(.borderedProminent) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - - private var emptyView: some View { - VStack(spacing: 16) { - Image(systemName: "tray") - .font(.system(size: 48)) - .foregroundColor(.gray) - - if displayMode == .matched { - Text("No Matched Filters") - .font(.headline) - - Text("No compact filters have matched any wallet addresses yet.\n\nFilters are checked during sync for relevant transactions.") - .font(.caption) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - .padding(.horizontal) - } else { - Text("No Compact Filters") - .font(.headline) - - Text("No compact filters have been downloaded yet.\n\nMake sure SPV sync is running.") - .font(.caption) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - .padding(.horizontal) - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } -} - -// MARK: - Filter Row Component - -struct FilterRow: View { - let filter: CompactFilter - let isExpanded: Bool - let isMatched: Bool - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - // Header row - HStack { - Image(systemName: isMatched ? "checkmark.circle.fill" : "line.3.horizontal.decrease.circle") - .foregroundColor(isMatched ? .green : .blue) - .font(.caption) - - Text("Height: \(filter.height)") - .font(.headline) - - Spacer() - - if isMatched { - Image(systemName: "star.fill") - .font(.caption2) - .foregroundColor(.orange) - } - - Text("\(filter.sizeInBytes) bytes") - .font(.caption) - .foregroundColor(.secondary) - - Image(systemName: isExpanded ? "chevron.up" : "chevron.down") - .font(.caption) - .foregroundColor(.secondary) - } - - // Expanded filter data - if isExpanded { - VStack(alignment: .leading, spacing: 8) { - // Filter size info - HStack { - Text("Size:") - .font(.caption) - .foregroundColor(.secondary) - Text("\(filter.sizeInBytes) bytes") - .font(.caption) - .foregroundColor(.primary) - Spacer() - } - - // Filter data hex preview - VStack(alignment: .leading, spacing: 4) { - HStack { - Text("Filter Data:") - .font(.caption) - .foregroundColor(.secondary) - Spacer() - Button(action: { - UIPasteboard.general.string = filter.data.hexEncodedString() - }) { - Image(systemName: "doc.on.doc") - .font(.caption2) - .foregroundColor(.blue) - } - .buttonStyle(.borderless) - } - - // Show first 32 bytes as hex preview - let previewBytes = min(32, filter.data.count) - if previewBytes > 0 { - Text(filter.data.prefix(previewBytes).hexEncodedString() + (filter.data.count > previewBytes ? "..." : "")) - .font(.system(.caption2, design: .monospaced)) - .foregroundColor(.primary) - .lineLimit(nil) - .padding(8) - .background(Color(.secondarySystemBackground)) - .cornerRadius(4) - } else { - Text("(empty)") - .font(.caption2) - .foregroundColor(.secondary) - .italic() - } - } - } - .padding(.top, 4) - } - } - .padding(.vertical, 4) - } -} - -// MARK: - Preview - -#if DEBUG -struct FilterMatchesView_Previews: PreviewProvider { - static var previews: some View { - NavigationStack { - FilterMatchesView(walletService: WalletService.shared) - .environmentObject(WalletService.shared) - } - } -} -#endif From e597cfbbf64d0c85dbb1d34529acea98875916ab Mon Sep 17 00:00:00 2001 From: Borja Castellano Date: Fri, 6 Feb 2026 13:29:38 -0600 Subject: [PATCH 03/15] removed block hits field and display --- .../SwiftDashSDK/Core/Services/WalletService.swift | 10 ++-------- .../SwiftExampleApp/Core/Views/CoreContentView.swift | 4 ---- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift index 03bab1d45ab..d2167a5c6c6 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift @@ -119,9 +119,7 @@ public class WalletService: ObservableObject { @Published var currentWallet: HDWallet? // Placeholder - use WalletManager instead @Published public var balance = Balance(confirmed: 0, unconfirmed: 0, immature: 0) @Published public var masternodesEnabled = true - - // Absolute heights for header sync display (current/target) - @Published public var blocksHit: Int = 0 + @Published public var lastSyncError: Error? private var activeSyncStartTimestamp: TimeInterval = 0 @@ -562,11 +560,7 @@ public class WalletService: ObservableObject { func onFilterStored(_ startHeight: UInt32, _ endHeight: UInt32) {} func onFilterSyncCompleted(_ tipHeight: UInt32) {} func onBlocksNeeded(_ height: UInt32, _ hash: Data, _ count: UInt32) {} - func onBlocksProcessed(_ height: UInt32, _ hash: Data, _ newAddressCount: UInt32) { - Task { @MainActor in - walletService.blocksHit += 1 - } - } + func onBlocksProcessed(_ height: UInt32, _ hash: Data, _ newAddressCount: UInt32) {} func onMasternodeStateUpdated(_ height: UInt32) {} func onChainLockReceived(_ height: UInt32, _ hash: Data, _ signature: Data, _ validated: Bool) {} func onInstantLockReceived(_ txid: Data, _ instantLockData: Data, _ validated: Bool) {} diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift index 66f03539afc..61b0717cfb6 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift @@ -90,10 +90,6 @@ var body: some View { // Controls row HStack(spacing: 8) { - Text("Blocks hit: \(walletService.blocksHit)") - .font(.caption2) - .foregroundColor(.secondary) - Spacer() Button(action: toggleSync) { From 8e779284954bd8e475639afae9625d2052712c0b Mon Sep 17 00:00:00 2001 From: Borja Castellano Date: Fri, 6 Feb 2026 13:56:01 -0600 Subject: [PATCH 04/15] removed balance field in WalletService --- .../Core/Services/WalletService.swift | 36 +++---------------- 1 file changed, 4 insertions(+), 32 deletions(-) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift index d2167a5c6c6..01f36fce13d 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift @@ -117,7 +117,6 @@ public class WalletService: ObservableObject { // Published properties @Published public private(set) var syncProgress: SPVSyncProgress = SPVSyncProgress.default() @Published var currentWallet: HDWallet? // Placeholder - use WalletManager instead - @Published public var balance = Balance(confirmed: 0, unconfirmed: 0, immature: 0) @Published public var masternodesEnabled = true @Published public var lastSyncError: Error? @@ -393,8 +392,7 @@ public class WalletService: ObservableObject { walletManager = nil currentWallet = nil transactions = [] - balance = Balance(confirmed: 0, unconfirmed: 0, immature: 0) - + // Reconfigure with new network if let modelContainer = modelContainer { configure(modelContainer: modelContainer, network: network) @@ -501,7 +499,6 @@ public class WalletService: ObservableObject { if currentWallet?.id == wallet.id { currentWallet = nil transactions = [] - balance = Balance(confirmed: 0, unconfirmed: 0, immature: 0) } // Remove wallet from observable state BEFORE SwiftData delete @@ -606,40 +603,15 @@ public class WalletService: ObservableObject { _ txid: Data, _ amount: Int64, _ addresses: [String] -<<<<<<< HEAD - ) { - Task { @MainActor in - if let wm = walletService.walletManager { - for wallet in wm.wallets { - await wm.syncWalletStateFromRust(for: wallet) - } - } - - if walletService.currentWallet != nil { - await walletService.loadTransactions() - } - } - } - -======= - ) { } - ->>>>>>> a72969687 (fixed the performance issue while sync) + ) {} + func onBalanceUpdated( _ walletId: String, _ spendable: UInt64, _ unconfirmed: UInt64, _ immature: UInt64, _ locked: UInt64 - ) { - Task { @MainActor in - walletService.balance = Balance( - confirmed: spendable, - unconfirmed: unconfirmed, - immature: immature - ) - } - } + ) {} } } From 813baae9a72c081cf4723a5afa6ded2ebbc113fe Mon Sep 17 00:00:00 2001 From: Borja Castellano Date: Fri, 6 Feb 2026 14:03:17 -0600 Subject: [PATCH 05/15] removed transactions field fom walletService --- .../Core/Services/WalletService.swift | 33 ++----------------- 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift index 01f36fce13d..75a6c012516 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift @@ -122,7 +122,6 @@ public class WalletService: ObservableObject { @Published public var lastSyncError: Error? private var activeSyncStartTimestamp: TimeInterval = 0 - @Published public var transactions: [CoreTransaction] = [] // Use HDTransaction from wallet @Published var currentNetwork: AppNetwork = .testnet // Internal properties @@ -294,10 +293,7 @@ public class WalletService: ObservableObject { public func loadWallet(_ wallet: HDWallet) async { currentWallet = wallet - - // Load transactions - await loadTransactions() - + // Update balance updateBalance() } @@ -391,7 +387,6 @@ public class WalletService: ObservableObject { // Clear current wallet manager walletManager = nil currentWallet = nil - transactions = [] // Reconfigure with new network if let modelContainer = modelContainer { @@ -439,30 +434,7 @@ public class WalletService: ObservableObject { return txid } - - private func loadTransactions() async { - guard let wallet = currentWallet else { return } - - // Convert HDTransaction to CoreTransaction - transactions = wallet.transactions.map { hdTx in - CoreTransaction( - id: hdTx.txHash, - amount: hdTx.amount, - fee: hdTx.fee, - timestamp: hdTx.timestamp, - blockHeight: hdTx.blockHeight != nil ? Int64(hdTx.blockHeight!) : nil, - confirmations: hdTx.confirmations, - type: hdTx.type, - memo: nil, - inputs: [], - outputs: [], - isInstantSend: hdTx.isInstantSend, - isAssetLock: false, - rawData: hdTx.rawTransaction - ) - }.sorted { $0.timestamp > $1.timestamp } - } - + // MARK: - Address Management public func getNewAddress() async throws -> String { @@ -498,7 +470,6 @@ public class WalletService: ObservableObject { // If this was the current wallet, clear it if currentWallet?.id == wallet.id { currentWallet = nil - transactions = [] } // Remove wallet from observable state BEFORE SwiftData delete From ab5248c2a5795c16826425086c6a1baa93bbb5ea Mon Sep 17 00:00:00 2001 From: Borja Castellano Date: Fri, 6 Feb 2026 16:38:34 -0600 Subject: [PATCH 06/15] WalletServices as a facade for CoreWalletManager and cleaned up wallet functionalities --- .../Core/Services/WalletService.swift | 180 +----------------- .../SwiftDashSDK/Core/Wallet/HDWallet.swift | 8 + .../Core/Views/AddressManagementView.swift | 161 ---------------- .../Core/Views/CreateWalletView.swift | 8 +- .../Core/Views/ReceiveAddressView.swift | 126 +++++------- .../Core/Views/SendTransactionView.swift | 23 +-- .../Core/Views/WalletDetailView.swift | 3 - 7 files changed, 71 insertions(+), 438 deletions(-) delete mode 100644 packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AddressManagementView.swift diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift index 75a6c012516..19bf03944ec 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift @@ -116,7 +116,6 @@ public class WalletService: ObservableObject { // Published properties @Published public private(set) var syncProgress: SPVSyncProgress = SPVSyncProgress.default() - @Published var currentWallet: HDWallet? // Placeholder - use WalletManager instead @Published public var masternodesEnabled = true @Published public var lastSyncError: Error? @@ -158,28 +157,7 @@ public class WalletService: ObservableObject { initializeNewSPVClient() SDKLogger.log("Loading current wallet...", minimumLevel: .medium) - - guard self.modelContainer != nil else { return } - - // The WalletManager will handle loading and restoring wallets from persistence - // It will restore the serialized wallet bytes to the FFI wallet manager - // This happens automatically in WalletManager.init() through loadWallets() - - // Just sync the current wallet from WalletManager - if let walletManager = self.walletManager { - Task { - // WalletManager's loadWallets() is called in its init - // We just need to sync the current wallet - if let wallet = walletManager.currentWallet { - self.currentWallet = wallet - await loadWallet(wallet) - } else if let firstWallet = walletManager.wallets.first { - self.currentWallet = firstWallet - await loadWallet(firstWallet) - } - } - } - + SDKLogger.log("=== WalletService.configure END ===", minimumLevel: .medium) } @@ -249,59 +227,15 @@ public class WalletService: ObservableObject { // MARK: - Wallet Management - public func createWallet(label: String, mnemonic: String? = nil, pin: String = "1234", isImport: Bool = false) async throws -> HDWallet { - print("=== WalletService.createWallet START ===") - print("Label: \(label)") - print("Has mnemonic: \(mnemonic != nil)") - print("PIN: \(pin)") - print("ModelContainer available: \(modelContainer != nil)") - - guard let walletManager = walletManager else { - print("ERROR: WalletManager not initialized") - print("WalletManager is nil") - throw WalletError.notImplemented("WalletManager not initialized") - } - - do { - // Create wallet using our refactored WalletManager that wraps FFI - print("WalletManager available, creating wallet...") - let wallet = try await walletManager.createWallet( - label: label, - mnemonic: mnemonic, - pin: pin, - isImport: isImport - ) - - print("Wallet created by WalletManager, ID: \(wallet.id)") - print("Loading wallet...") - - // Load the newly created wallet - await loadWallet(wallet) - - // Persist sync-from changes - try modelContainer?.mainContext.save() - - print("=== WalletService.createWallet SUCCESS ===") - return wallet - } catch { - print("=== WalletService.createWallet FAILED ===") - print("Error type: \(type(of: error))") - print("Error: \(error)") - throw error - } - } - - public func loadWallet(_ wallet: HDWallet) async { - currentWallet = wallet - - // Update balance - updateBalance() + public func createWallet(label: String, mnemonic: String, pin: String, isImport: Bool) async throws -> HDWallet { + return try await walletManager!.createWallet( + label: label, + mnemonic: mnemonic, + pin: pin, + isImport: isImport + ) } - - // Placeholder for balance update logic (i think events manage - // this but have to confirm) - private func updateBalance() {} - + // MARK: - Trusted Mode / Masternode Sync public func setMasternodesEnabled(_ enabled: Bool) { masternodesEnabled = enabled @@ -386,7 +320,6 @@ public class WalletService: ObservableObject { // Clear current wallet manager walletManager = nil - currentWallet = nil // Reconfigure with new network if let modelContainer = modelContainer { @@ -396,101 +329,8 @@ public class WalletService: ObservableObject { print("=== WalletService.switchNetwork END ===") } - // MARK: - Address Management - - public func generateAddresses(for account: HDAccount, count: Int, type: AddressType) async throws { - guard let walletManager = self.walletManager else { - throw WalletError.notImplemented("WalletManager not available") - } - - try await walletManager.generateAddresses(for: account, count: count, type: type) - try? modelContainer?.mainContext.save() - } - - // MARK: - Transaction Management - - public func sendTransaction(to address: String, amount: UInt64, memo: String? = nil) async throws -> String { - guard let wallet = currentWallet else { - throw WalletError.notImplemented("No active wallet") - } - - guard wallet.confirmedBalance >= amount else { - throw WalletError.notImplemented("Insufficient funds") - } - - // Mock transaction creation - let txid = UUID().uuidString - let transaction = HDTransaction(txHash: txid, timestamp: Date()) - transaction.amount = -Int64(amount) - transaction.fee = 1000 - transaction.type = "sent" - transaction.wallet = wallet - - modelContainer?.mainContext.insert(transaction) - try? modelContainer?.mainContext.save() - - // Update balance - updateBalance() - - return txid - } - - // MARK: - Address Management - - public func getNewAddress() async throws -> String { - guard let wallet = currentWallet else { - throw WalletError.notImplemented("No active wallet") - } - - // Find next unused address or create new one - let currentAccount = wallet.accounts.first ?? wallet.createAccount() - let existingAddresses = currentAccount.externalAddresses - let nextIndex = UInt32(existingAddresses.count) - - // Mock address generation - let address = "yMockAddress\(nextIndex)" - - let hdAddress = HDAddress( - address: address, - index: nextIndex, - derivationPath: "m/44'/5'/0'/0/\(nextIndex)", - addressType: .external, - account: currentAccount - ) - - modelContainer?.mainContext.insert(hdAddress) - try? modelContainer?.mainContext.save() - - return address - } - - // MARK: - Wallet Deletion - public func walletDeleted(_ wallet: HDWallet) async { - // If this was the current wallet, clear it - if currentWallet?.id == wallet.id { - currentWallet = nil - } - - // Remove wallet from observable state BEFORE SwiftData delete - // This prevents "Never access a full future backing data" crash - if let walletManager = walletManager { - walletManager.removeWalletFromObservableState(wallet) - - // Set a new current wallet if available - if currentWallet == nil, let firstWallet = walletManager.wallets.first { - await loadWallet(firstWallet) - } - } - } - - // MARK: - Helpers - - private func generateMnemonic() -> String { - // Mock mnemonic generation - let words = ["abandon", "ability", "able", "about", "above", "absent", - "absorb", "abstract", "absurd", "abuse", "access", "accident"] - return words.joined(separator: " ") + await walletManager!.removeWalletFromObservableState(wallet) } // MARK: - SPV Event Handlers implementations diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/HDWallet.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/HDWallet.swift index fc2d75353c8..3edb6dcc7db 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/HDWallet.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/HDWallet.swift @@ -96,6 +96,14 @@ public final class HDWallet: HDWalletModels { accounts.append(account) return account } + + public func getAddress() -> HDAddress? { + guard let currentAccount = self.accounts.first else { + return nil + } + + return currentAccount.externalAddresses.last + } } // MARK: - HD Account diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AddressManagementView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AddressManagementView.swift deleted file mode 100644 index d44e8ea789a..00000000000 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AddressManagementView.swift +++ /dev/null @@ -1,161 +0,0 @@ -import SwiftUI -import SwiftDashSDK - -struct AddressManagementView: View { - @EnvironmentObject var walletService: WalletService - let account: HDAccount - @State private var selectedType: AddressType = .external - @State private var isGenerating = false - @State private var error: Error? - - var body: some View { - VStack(spacing: 0) { - // Address Type Picker - Picker("Address Type", selection: $selectedType) { - Text("External").tag(AddressType.external) - Text("Internal").tag(AddressType.internal) - Text("CoinJoin").tag(AddressType.coinJoin) - Text("Identity").tag(AddressType.identity) - } - .pickerStyle(.segmented) - .padding() - - // Address List - List { - ForEach(addressesForType(selectedType)) { address in - AddressDetailRow(address: address) - } - - // Generate More Button - Section { - Button { - generateMoreAddresses() - } label: { - HStack { - Image(systemName: "plus.circle.fill") - Text("Generate More Addresses") - } - } - .disabled(isGenerating) - } - } - .listStyle(.grouped) - } - .navigationTitle("Address Management") - .navigationBarTitleDisplayMode(.inline) - .alert("Error", isPresented: .constant(error != nil)) { - Button("OK") { - error = nil - } - } message: { - if let error = error { - Text(error.localizedDescription) - } - } - } - - private func addressesForType(_ type: AddressType) -> [HDAddress] { - switch type { - case .external: - return account.externalAddresses - case .internal: - return account.internalAddresses - case .coinJoin: - return account.coinJoinAddresses - case .identity: - return account.identityFundingAddresses - } - } - - private func generateMoreAddresses() { - isGenerating = true - - Task { - do { - try await walletService.generateAddresses(for: account, count: 10, type: selectedType) - await MainActor.run { - isGenerating = false - } - } catch { - await MainActor.run { - self.error = error - isGenerating = false - } - } - } - } -} - -struct AddressDetailRow: View { - let address: HDAddress - @State private var copied = false - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - HStack { - VStack(alignment: .leading, spacing: 4) { - Text("Address #\(address.index)") - .font(.headline) - - Text(address.derivationPath) - .font(.caption2) - .foregroundColor(.secondary) - } - - Spacer() - - VStack(alignment: .trailing, spacing: 4) { - if address.isUsed { - Label("Used", systemImage: "checkmark.circle.fill") - .font(.caption) - .foregroundColor(.green) - } - - if address.balance > 0 { - Text(formatBalance(address.balance)) - .font(.caption) - .fontWeight(.medium) - } - } - } - - HStack { - Text(address.address) - .font(.system(.caption, design: .monospaced)) - .foregroundColor(.secondary) - .lineLimit(1) - .truncationMode(.middle) - - Button { - copyAddress() - } label: { - Image(systemName: copied ? "checkmark" : "doc.on.doc") - .foregroundColor(.accentColor) - } - .buttonStyle(.plain) - } - - if let lastSeenTime = address.lastSeenTime { - Text("Last seen: \(lastSeenTime, style: .relative)") - .font(.caption2) - .foregroundColor(.secondary) - } - } - .padding(.vertical, 4) - } - - private func copyAddress() { - UIPasteboard.general.string = address.address - copied = true - - Task { - try? await Task.sleep(nanoseconds: 2_000_000_000) - copied = false - } - } - - private func formatBalance(_ amount: UInt64) -> String { - let dash = Double(amount) / 100_000_000.0 - return String(format: "%.8f DASH", dash) - } -} diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift index 34396dd95f1..9a3ac21c6ed 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift @@ -253,7 +253,7 @@ struct CreateWalletView: View { } } - private func createWallet(using mnemonic: String?) { + private func createWallet(using mnemonic: String) { guard !walletLabel.isEmpty, walletPin == confirmPin, walletPin.count >= 4 && walletPin.count <= 6 else { @@ -269,9 +269,9 @@ struct CreateWalletView: View { Task { do { print("=== STARTING WALLET CREATION ===") - - let mnemonic: String? = (showImportOption ? importMnemonic : mnemonic) - print("Has mnemonic: \(mnemonic != nil)") + + let mnemonic = (showImportOption ? importMnemonic : mnemonic) + print("Has mnemonic: \(mnemonic)") print("PIN length: \(walletPin.count)") print("Import option enabled: \(showImportOption)") diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/ReceiveAddressView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/ReceiveAddressView.swift index 22df2575437..58f50d23a8d 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/ReceiveAddressView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/ReceiveAddressView.swift @@ -6,65 +6,58 @@ struct ReceiveAddressView: View { @Environment(\.dismiss) private var dismiss @EnvironmentObject var walletService: WalletService let wallet: HDWallet - - @State private var currentAddress: String = "" - @State private var isLoadingAddress = false + @State private var copiedToClipboard = false var body: some View { NavigationStack { VStack(spacing: 24) { - if isLoadingAddress { - ProgressView("Generating address...") - .frame(maxWidth: .infinity, maxHeight: .infinity) - } else if !currentAddress.isEmpty { - VStack(spacing: 24) { - // QR Code - if let qrImage = generateQRCode(from: currentAddress) { - Image(uiImage: qrImage) - .interpolation(.none) - .resizable() - .scaledToFit() - .frame(width: 250, height: 250) - .padding() - .background(Color.white) - .cornerRadius(12) - } - - // Address - VStack(spacing: 12) { - Text("Your Dash Address") - .font(.subheadline) - .foregroundColor(.secondary) - - Text(currentAddress) - .font(.system(.body, design: .monospaced)) - .multilineTextAlignment(.center) - .padding() - .background(Color(UIColor.secondarySystemBackground)) - .cornerRadius(8) - .onTapGesture { - copyToClipboard() - } - } - .padding(.horizontal) - - // Copy Button - Button { - copyToClipboard() - } label: { - Label( - copiedToClipboard ? "Copied!" : "Copy Address", - systemImage: copiedToClipboard ? "checkmark" : "doc.on.doc" - ) - .frame(maxWidth: .infinity) + let currentAddress = wallet.getAddress()?.address ?? "" + + // QR Code + if let qrImage = generateQRCode(from: currentAddress) { + Image(uiImage: qrImage) + .interpolation(.none) + .resizable() + .scaledToFit() + .frame(width: 250, height: 250) + .padding() + .background(Color.white) + .cornerRadius(12) + } + + // Address + VStack(spacing: 12) { + Text("Your Dash Address") + .font(.subheadline) + .foregroundColor(.secondary) + + Text(currentAddress) + .font(.system(.body, design: .monospaced)) + .multilineTextAlignment(.center) + .padding() + .background(Color(UIColor.secondarySystemBackground)) + .cornerRadius(8) + .onTapGesture { + copyToClipboard(currentAddress) } - .buttonStyle(.borderedProminent) - .padding(.horizontal) - - Spacer() - } } + .padding(.horizontal) + + // Copy Button + Button { + copyToClipboard(currentAddress) + } label: { + Label( + copiedToClipboard ? "Copied!" : "Copy Address", + systemImage: copiedToClipboard ? "checkmark" : "doc.on.doc" + ) + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .padding(.horizontal) + + Spacer() } .navigationTitle("Receive Dash") .navigationBarTitleDisplayMode(.inline) @@ -76,29 +69,6 @@ struct ReceiveAddressView: View { } } } - .task { - await loadAddress() - } - } - - private func loadAddress() async { - isLoadingAddress = true - - // Try to get existing receive address or generate new one - if let currentAccount = wallet.accounts.first, - let lastAddress = currentAccount.externalAddresses.last { - currentAddress = lastAddress.address - } else { - do { - currentAddress = try await walletService.getNewAddress() - } catch { - // Use a mock address for now - let addressCount = wallet.accounts.first?.externalAddresses.count ?? 0 - currentAddress = "yMockReceiveAddress\(addressCount)" - } - } - - isLoadingAddress = false } private func generateQRCode(from string: String) -> UIImage? { @@ -118,9 +88,9 @@ struct ReceiveAddressView: View { return nil } - - private func copyToClipboard() { - UIPasteboard.general.string = currentAddress + + private func copyToClipboard(_ string: String) { + UIPasteboard.general.string = string copiedToClipboard = true // Reset after 2 seconds diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift index 183bbb8d3fe..f4c8dda7b91 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift @@ -123,28 +123,7 @@ struct SendTransactionView: View { } private func sendTransaction() { - guard let amount = amount else { return } - - isSending = true - - Task { - do { - let txid = try await walletService.sendTransaction( - to: recipientAddress, - amount: amount, - memo: memo.isEmpty ? nil : memo - ) - - await MainActor.run { - successTxid = txid - } - } catch { - await MainActor.run { - self.error = error - isSending = false - } - } - } + // TODO: Send transactions is not yet implemented } private func formatBalance(_ amount: UInt64) -> String { diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift index 7fbe0288b3d..5c67a31e05d 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift @@ -140,9 +140,6 @@ struct WalletDetailView: View { } .environmentObject(walletService) } - .task { - await walletService.loadWallet(wallet) - } .onAppear { unifiedAppState.showWalletsSyncDetails = false } } } From 0094f62884ba4b42f8a10c1b5b27e690771ccdb6 Mon Sep 17 00:00:00 2001 From: Borja Castellano Date: Fri, 6 Feb 2026 16:41:22 -0600 Subject: [PATCH 07/15] dropped withTimeout helper in WalletService --- .../Core/Services/WalletService.swift | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift index 19bf03944ec..3320223a38f 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift @@ -2,30 +2,6 @@ import Foundation import SwiftData import Combine -// MARK: - Timeout Helper - -struct TimeoutError: Error {} - -func withTimeout(seconds: TimeInterval, operation: @escaping @Sendable () async throws -> T) async throws -> T { - try await withThrowingTaskGroup(of: T.self) { group in - // Add the actual operation - group.addTask { - try await operation() - } - - // Add timeout task - group.addTask { - try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) - throw TimeoutError() - } - - // Return first result (either completion or timeout) - let result = try await group.next()! - group.cancelAll() - return result - } -} - // MARK: - Logging Preferences public enum LoggingPreset: String { From 71f6408f7b12d89e2a0ab2a5a476239ffe511438 Mon Sep 17 00:00:00 2001 From: Borja Castellano Date: Fri, 6 Feb 2026 16:44:56 -0600 Subject: [PATCH 08/15] fields cleaned and reorg in WalletService --- .../Core/Services/WalletService.swift | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift index 3320223a38f..0c8915e1560 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift @@ -86,30 +86,22 @@ func print(_ items: Any..., separator: String = " ", terminator: String = "\n") @MainActor public class WalletService: ObservableObject { - // Sendable wrapper to move non-Sendable references across actor boundaries when safe - private final class SendableBox: @unchecked Sendable { let value: T; init(_ v: T) { self.value = v } } public static let shared = WalletService() // Published properties @Published public private(set) var syncProgress: SPVSyncProgress = SPVSyncProgress.default() @Published public var masternodesEnabled = true - @Published public var lastSyncError: Error? - - private var activeSyncStartTimestamp: TimeInterval = 0 @Published var currentNetwork: AppNetwork = .testnet // Internal properties private var modelContainer: ModelContainer? - - // Exposed for WalletViewModel - read-only access to the properly initialized WalletManager - public private(set) var walletManager: CoreWalletManager? - + // SPV Client - new wrapper with proper sync support private var spvClient: SPVClient? - - // Mock SDK for now - will be replaced with real SDK - private var sdk: Any? + + // Exposed for WalletViewModel - read-only access to the properly initialized WalletManager + public private(set) var walletManager: CoreWalletManager? private init() {} @@ -136,12 +128,7 @@ public class WalletService: ObservableObject { SDKLogger.log("=== WalletService.configure END ===", minimumLevel: .medium) } - - public func setSharedSDK(_ sdk: Any) { - self.sdk = sdk - SDKLogger.log("✅ WalletService configured with shared SDK", minimumLevel: .medium) - } - + private func initializeNewSPVClient() { SDKLogger.log("Initializing SPV Client for \(self.currentNetwork.rawValue)...", minimumLevel: .medium) From 4680046e7a5d50dd1435701ebc39e06aadcca4ce Mon Sep 17 00:00:00 2001 From: Borja Castellano Date: Fri, 6 Feb 2026 17:33:22 -0600 Subject: [PATCH 09/15] refactoring wallet service optional fields into not optional --- .../Core/Services/WalletService.swift | 225 ++++++++---------- .../Core/Wallet/CoreWalletManager.swift | 2 +- .../Core/Views/AccountDetailView.swift | 10 +- .../Core/Views/AccountListView.swift | 2 +- .../Core/Views/CreateWalletView.swift | 4 + .../Core/Views/TransactionListView.swift | 8 +- .../Core/Views/WalletDetailView.swift | 9 +- .../SwiftExampleApp/UnifiedAppState.swift | 6 +- 8 files changed, 116 insertions(+), 150 deletions(-) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift index 0c8915e1560..60f67f7bcb7 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift @@ -84,119 +84,107 @@ func print(_ items: Any..., separator: String = " ", terminator: String = "\n") Swift.print(output, terminator: terminator) } +// DESIGN NOTE: This class feels like something that should be in the example app, +// we, as sdk developers, provide the tools and ffi wrappers, but how to +// use them depends on the sdk user, for example, by implementing the SPV event +// handlers, the user can decide what to do with the events, but if we implement them in the sdk +// we are taking that decision for them, and maybe not all users want the same thing @MainActor public class WalletService: ObservableObject { - public static let shared = WalletService() - // Published properties @Published public private(set) var syncProgress: SPVSyncProgress = SPVSyncProgress.default() @Published public var masternodesEnabled = true @Published public var lastSyncError: Error? - @Published var currentNetwork: AppNetwork = .testnet - - // Internal properties - private var modelContainer: ModelContainer? + @Published var network: AppNetwork - // SPV Client - new wrapper with proper sync support - private var spvClient: SPVClient? + // Internal properties + private var modelContainer: ModelContainer - // Exposed for WalletViewModel - read-only access to the properly initialized WalletManager - public private(set) var walletManager: CoreWalletManager? - - private init() {} - - deinit { - // Avoid capturing self across an async boundary; capture the client locally - guard let client = spvClient else { return } - Task { @MainActor in - client.stopSync() - client.destroy() - } - } + // SPV Client and Wallet wrappers + private var spvClient: SPVClient + public private(set) var walletManager: CoreWalletManager - public func configure(modelContainer: ModelContainer, network: AppNetwork = .testnet) { - LoggingPreferences.configure() - SDKLogger.log("=== WalletService.configure START ===", minimumLevel: .medium) + public init(modelContainer: ModelContainer, network: AppNetwork) { self.modelContainer = modelContainer - self.currentNetwork = network - SDKLogger.log("ModelContainer set: \(modelContainer)", minimumLevel: .high) - SDKLogger.log("Network set: \(network.rawValue)", minimumLevel: .medium) - - initializeNewSPVClient() - - SDKLogger.log("Loading current wallet...", minimumLevel: .medium) + self.network = network - SDKLogger.log("=== WalletService.configure END ===", minimumLevel: .medium) + LoggingPreferences.configure() + + let dataDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("SPV").appendingPathComponent(network.rawValue).path + + // For simplicity, lets unwrap the error. This can only fail due to + // IO errors when working with the internal storage system, I don't + // see how we can recover from that right now easily + let spvClient = try! SPVClient( + network: network.sdkNetwork, + dataDir: dataDir, + startHeight: 0, + ) + + self.spvClient = spvClient + + // Create the SDK wallet manager by reusing the SPV client's shared manager + // TODO: Investigate this errors + let sdkWalletManager = try! spvClient.getWalletManager() + let wrapper = try! CoreWalletManager(sdkWalletManager: sdkWalletManager, modelContainer: modelContainer) + + wrapper.transactionService = TransactionService( + walletManager: wrapper, + modelContainer: modelContainer, + spvClient: spvClient + ) + + self.walletManager = wrapper + + spvClient.setProgressUpdateEventHandler(SPVProgressUpdateEventHandlerImpl(walletService: self)) + spvClient.setSyncEventsHandler(SPVSyncEventsHandlerImpl(walletService: self)) + spvClient.setNetworkEventsHandler(SPVNetworkEventsHandlerImpl(walletService: self)) + spvClient.setWalletEventsHandler(SPVWalletEventsHandlerImpl(walletService: self)) + } + + deinit { + spvClient.stopSync() + spvClient.destroy() } private func initializeNewSPVClient() { - SDKLogger.log("Initializing SPV Client for \(self.currentNetwork.rawValue)...", minimumLevel: .medium) - - let dataDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("SPV").appendingPathComponent(self.currentNetwork.rawValue).path - // Currently always starting at 0 for simplicity. While this is - // currently configurable, the SPVClient should decide using the wallet - // creation time to determine the start height, removing usage complexity - // and possible missusage errors - let startHeight: UInt32 = 0 - let net = currentNetwork - - SDKLogger.log("[SPV][Baseline] Using baseline startFromHeight=\(startHeight) on \(net.rawValue) during initialize()", minimumLevel: .high) - - do { - // This ensures no memory leaks when creating a new client - // and unlocks the storage in case we are about to use the same (we are) - if self.spvClient != nil { - self.spvClient!.destroy() - } - - spvClient = try SPVClient( - network: self.currentNetwork.sdkNetwork, - dataDir: dataDir, - startHeight: startHeight, - ) - - spvClient?.setProgressUpdateEventHandler(SPVProgressUpdateEventHandlerImpl(walletService: self)) - spvClient?.setSyncEventsHandler(SPVSyncEventsHandlerImpl(walletService: self)) - spvClient?.setNetworkEventsHandler(SPVNetworkEventsHandlerImpl(walletService: self)) - spvClient?.setWalletEventsHandler(SPVWalletEventsHandlerImpl(walletService: self)) - - try spvClient?.setMasternodeSyncEnabled(self.masternodesEnabled) - } catch { - SDKLogger.error("Failed to initialize SPV Client: \(error)") - self.lastSyncError = error - return - } - - SDKLogger.log("✅ SPV Client initialized successfully for \(net.rawValue) (deferred start)", minimumLevel: .medium) - - // Capture current references on the main actor to avoid cross-actor hops later - guard let client = spvClient, let mc = self.modelContainer else { return } - + SDKLogger.log("Initializing SPV Client for \(self.self.network.rawValue)...", minimumLevel: .medium) + + let dataDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("SPV").appendingPathComponent(self.network.rawValue).path + + // This ensures no memory leaks when creating a new client + // and unlocks the storage in case we are about to use the same (we probably are) + self.spvClient.destroy() + + // For simplicity, lets unwrap the error. This can only fail due to + // IO errors when working with the internal storage system, I don't + // see how we can recover from that right now easily + self.spvClient = try! SPVClient( + network: self.self.network.sdkNetwork, + dataDir: dataDir, + startHeight: 0, + ) + + self.spvClient.setProgressUpdateEventHandler(SPVProgressUpdateEventHandlerImpl(walletService: self)) + self.spvClient.setSyncEventsHandler(SPVSyncEventsHandlerImpl(walletService: self)) + self.spvClient.setNetworkEventsHandler(SPVNetworkEventsHandlerImpl(walletService: self)) + self.spvClient.setWalletEventsHandler(SPVWalletEventsHandlerImpl(walletService: self)) + + try! self.spvClient.setMasternodeSyncEnabled(self.masternodesEnabled) + + SDKLogger.log("✅ SPV Client initialized successfully for \(self.network.rawValue) (deferred start)", minimumLevel: .medium) + // Create the SDK wallet manager by reusing the SPV client's shared manager - do { - let sdkWalletManager = try client.getWalletManager() - let wrapper = try CoreWalletManager(sdkWalletManager: sdkWalletManager, modelContainer: mc) - self.walletManager = wrapper - self.walletManager?.transactionService = TransactionService( - walletManager: wrapper, - modelContainer: mc, - spvClient: client - ) - SDKLogger.log("✅ WalletManager wrapper initialized successfully", minimumLevel: .medium) - } catch { - SDKLogger.error("❌ Failed to initialize WalletManager wrapper:\nError: \(error)") - } - } - - // MARK: - Wallet Management - - public func createWallet(label: String, mnemonic: String, pin: String, isImport: Bool) async throws -> HDWallet { - return try await walletManager!.createWallet( - label: label, - mnemonic: mnemonic, - pin: pin, - isImport: isImport - ) + // TODO: Investigate this errors + let sdkWalletManager = try! spvClient.getWalletManager() + let wrapper = try! CoreWalletManager(sdkWalletManager: sdkWalletManager, modelContainer: modelContainer) + self.walletManager = wrapper + self.walletManager.transactionService = TransactionService( + walletManager: wrapper, + modelContainer: modelContainer, + spvClient: spvClient + ) + SDKLogger.log("✅ WalletManager wrapper initialized successfully", minimumLevel: .medium) } // MARK: - Trusted Mode / Masternode Sync @@ -204,7 +192,7 @@ public class WalletService: ObservableObject { masternodesEnabled = enabled // Try to apply immediately if the client exists - do { try spvClient?.setMasternodeSyncEnabled(enabled) } catch { /* ignore */ } + do { try spvClient.setMasternodeSyncEnabled(enabled) } catch { /* ignore */ } } public func disableMasternodeSync() { setMasternodesEnabled(false) @@ -216,11 +204,6 @@ public class WalletService: ObservableObject { // MARK: - Sync Management public func startSync() async { - guard let spvClient = spvClient else { - print("❌ SPV Client not initialized") - return - } - lastSyncError = nil do { @@ -234,12 +217,9 @@ public class WalletService: ObservableObject { public func stopSync() { // pausing and resuming is not supported so, the trick is the following, // stop the old client and create a new one in its initial state xd - guard let client = spvClient else { return } - - client.stopSync() - - initializeNewSPVClient() - + spvClient.stopSync() + + self.initializeNewSPVClient() } public func clearSpvStorage() { @@ -248,9 +228,6 @@ public class WalletService: ObservableObject { return } - guard let spvClient = spvClient else { return } - - print("[SPV][Clear] Starting storage clear operation...") do { @@ -273,29 +250,17 @@ public class WalletService: ObservableObject { // MARK: - Network Management public func switchNetwork(to network: AppNetwork) async { - guard network != currentNetwork else { return } - currentNetwork = network - + guard network != self.network else { return } + self.network = network + print("=== WalletService.switchNetwork START ===") - print("Switching from \(currentNetwork.rawValue) to \(network.rawValue)") + print("Switching from \(self.network.rawValue) to \(network.rawValue)") self.stopSync() - - // Clear current wallet manager - walletManager = nil - // Reconfigure with new network - if let modelContainer = modelContainer { - configure(modelContainer: modelContainer, network: network) - } - print("=== WalletService.switchNetwork END ===") } - - public func walletDeleted(_ wallet: HDWallet) async { - await walletManager!.removeWalletFromObservableState(wallet) - } - + // MARK: - SPV Event Handlers implementations internal final class SPVProgressUpdateEventHandlerImpl: SPVProgressUpdateEventHandler, Sendable { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift index 6fdf32ef6c2..f3644cfeb61 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift @@ -56,7 +56,7 @@ public class CoreWalletManager: ObservableObject { } // MARK: - Wallet Management - func createWallet(label: String, mnemonic: String? = nil, pin: String, isImport: Bool = false) async throws -> HDWallet { + public func createWallet(label: String, mnemonic: String? = nil, pin: String, isImport: Bool = false) async throws -> HDWallet { print("WalletManager.createWallet called") isLoading = true defer { isLoading = false } diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountDetailView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountDetailView.swift index 2497f88dfd9..dd5d9774023 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountDetailView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountDetailView.swift @@ -604,10 +604,7 @@ struct AccountDetailView: View { private func derivePrivateKeyWithPIN(for detail: AddressDetail, pin: String) async { do { // Gate with PIN but derive via account-based FFI (no seed passage required) - guard let walletManager = walletService.walletManager else { - throw WalletError.walletError("Wallet manager not available") - } - let wifPrivateKey = try await walletManager.derivePrivateKeyAsWIF( + let wifPrivateKey = try await walletService.walletManager.derivePrivateKeyAsWIF( for: wallet, accountInfo: account, addressIndex: detail.index @@ -630,12 +627,15 @@ struct AccountDetailView: View { errorMessage = nil do { +<<<<<<< HEAD guard let walletManager = walletService.walletManager else { throw WalletError.walletError("Wallet manager not available") } +======= +>>>>>>> db8ce6d36 (refactoring wallet service optional fields into not optional) // Get extended public key and other details - let details = try await walletManager.getAccountDetails( + let details = try await walletService.walletManager.getAccountDetails( for: wallet, accountInfo: account ) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountListView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountListView.swift index def66fcf5e3..0ccfb1b5376 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountListView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountListView.swift @@ -52,7 +52,7 @@ struct AccountListView: View { do { // Get accounts from wallet manager - let fetchedAccounts = try await walletService.walletManager?.getAccounts(for: wallet) ?? [] + let fetchedAccounts = try await walletService.walletManager.getAccounts(for: wallet) await MainActor.run { self.accounts = fetchedAccounts self.isLoading = false diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift index 9a3ac21c6ed..a9bb65f6475 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift @@ -286,8 +286,12 @@ struct CreateWalletView: View { throw WalletError.walletError("No network selected") } +<<<<<<< HEAD // Create exactly one wallet in the SDK; do not append network to label _ = try await walletService.createWallet( +======= + let _ = try await walletService.walletManager.createWallet( +>>>>>>> db8ce6d36 (refactoring wallet service optional fields into not optional) label: walletLabel, mnemonic: mnemonic, pin: walletPin, diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/TransactionListView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/TransactionListView.swift index 62744cba519..43f0603f669 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/TransactionListView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/TransactionListView.swift @@ -83,14 +83,8 @@ struct TransactionListView: View { defer { isLoading = false } do { - // Get wallet manager - guard let walletManager = walletService.walletManager else { - throw NSError(domain: "TransactionListView", code: 1, - userInfo: [NSLocalizedDescriptionKey: "Wallet manager not initialized"]) - } - // Get transactions from the wallet manager - let fetchedTransactions = try await walletManager.getTransactions(for: wallet) + let fetchedTransactions = try await walletService.walletManager.getTransactions(for: wallet) await MainActor.run { self.transactions = fetchedTransactions diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift index 5c67a31e05d..b1d08570c48 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift @@ -385,21 +385,20 @@ struct WalletInfoView: View { private func loadAccountCounts() async { // TODO: This can probably be refactored now with with single network manager? - guard let manager = walletService.walletManager else { return } if mainnetEnabled { - if let list = try? await manager.getAccounts(for: wallet) { + if let list = try? await walletService.walletManager.getAccounts(for: wallet) { mainnetAccountCount = list.count } } else { mainnetAccountCount = nil } if testnetEnabled { - if let list = try? await manager.getAccounts(for: wallet) { + if let list = try? await walletService.walletManager.getAccounts(for: wallet) { testnetAccountCount = list.count } } else { testnetAccountCount = nil } if devnetEnabled { - if let list = try? await manager.getAccounts(for: wallet) { + if let list = try? await walletService.walletManager.getAccounts(for: wallet) { devnetAccountCount = list.count } } else { devnetAccountCount = nil } @@ -466,7 +465,7 @@ struct WalletInfoView: View { } // Notify the wallet service (removes wallet from observable arrays) - await walletService.walletDeleted(wallet) + walletService.walletManager.removeWalletFromObservableState(wallet) // Now safe to delete from Core Data (cascade will delete accounts/addresses) modelContext.delete(wallet) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/UnifiedAppState.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/UnifiedAppState.swift index 86dafecd654..693a2b342a9 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/UnifiedAppState.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/UnifiedAppState.swift @@ -52,12 +52,16 @@ class UnifiedAppState: ObservableObject { } // Initialize services - self.walletService = WalletService.shared self.platformState = AppState() +<<<<<<< HEAD // Configure wallet service with the current network from platform state self.walletService.configure(modelContainer: modelContainer, network: platformState.currentNetwork) +======= + self.walletService = WalletService(modelContainer: modelContainer, network: platformState.currentNetwork) + +>>>>>>> db8ce6d36 (refactoring wallet service optional fields into not optional) // Initialize unified state (will be updated with real SDKs during async init) self.unifiedState = UnifiedStateManager() } From 410f9937c24fc4e8c2f26248c52e2ee7a540d9ed Mon Sep 17 00:00:00 2001 From: Borja Castellano Date: Mon, 9 Feb 2026 13:19:34 -0600 Subject: [PATCH 10/15] CoreWalletManager init updated and dropped unused transaction stuff --- .../Core/Services/WalletService.swift | 25 +-- .../Core/Wallet/CoreWalletManager.swift | 29 +-- .../Core/Wallet/HDTransaction.swift | 192 ++---------------- .../Core/Wallet/TransactionErrors.swift | 73 ------- .../Core/Wallet/TransactionService.swift | 148 -------------- .../Core/Views/CreateWalletView.swift | 4 - 6 files changed, 29 insertions(+), 442 deletions(-) delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/TransactionErrors.swift delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/TransactionService.swift diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift index 60f67f7bcb7..9d95995dfbd 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift @@ -124,17 +124,8 @@ public class WalletService: ObservableObject { self.spvClient = spvClient // Create the SDK wallet manager by reusing the SPV client's shared manager - // TODO: Investigate this errors - let sdkWalletManager = try! spvClient.getWalletManager() - let wrapper = try! CoreWalletManager(sdkWalletManager: sdkWalletManager, modelContainer: modelContainer) - - wrapper.transactionService = TransactionService( - walletManager: wrapper, - modelContainer: modelContainer, - spvClient: spvClient - ) - - self.walletManager = wrapper + // TODO: Investigate this error + self.walletManager = try! CoreWalletManager(spvClient: spvClient, modelContainer: modelContainer) spvClient.setProgressUpdateEventHandler(SPVProgressUpdateEventHandlerImpl(walletService: self)) spvClient.setSyncEventsHandler(SPVSyncEventsHandlerImpl(walletService: self)) @@ -175,15 +166,9 @@ public class WalletService: ObservableObject { SDKLogger.log("✅ SPV Client initialized successfully for \(self.network.rawValue) (deferred start)", minimumLevel: .medium) // Create the SDK wallet manager by reusing the SPV client's shared manager - // TODO: Investigate this errors - let sdkWalletManager = try! spvClient.getWalletManager() - let wrapper = try! CoreWalletManager(sdkWalletManager: sdkWalletManager, modelContainer: modelContainer) - self.walletManager = wrapper - self.walletManager.transactionService = TransactionService( - walletManager: wrapper, - modelContainer: modelContainer, - spvClient: spvClient - ) + // TODO: Investigate this error + self.walletManager = try! CoreWalletManager(spvClient: self.spvClient, modelContainer: self.modelContainer) + SDKLogger.log("✅ WalletManager wrapper initialized successfully", minimumLevel: .medium) } diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift index f3644cfeb61..9e59bf7f065 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift @@ -17,37 +17,20 @@ public class CoreWalletManager: ObservableObject { @Published public private(set) var error: WalletError? // SDK wallet manager - this is the real wallet manager from the SDK - private let sdkWalletManager: SwiftDashSDK.WalletManager + private let sdkWalletManager: WalletManager private let modelContainer: ModelContainer private let storage = WalletStorage() - - // Services (initialize in WalletService when SPV is available) - var transactionService: TransactionService? - + /// Initialize with an SDK wallet manager /// - Parameters: /// - sdkWalletManager: The SDK wallet manager from SwiftDashSDK /// - modelContainer: SwiftData model container for persistence - init(sdkWalletManager: SwiftDashSDK.WalletManager, modelContainer: ModelContainer? = nil) throws { + init(spvClient: SPVClient, modelContainer: ModelContainer) throws { print("=== WalletManager.init START ===") + + self.sdkWalletManager = try spvClient.getWalletManager() + self.modelContainer = modelContainer - self.sdkWalletManager = sdkWalletManager - - if let container = modelContainer { - print("Using provided ModelContainer") - self.modelContainer = container - } else { - do { - print("Creating ModelContainer...") - self.modelContainer = try ModelContainer(for: HDWallet.self, HDAccount.self, HDAddress.self, HDUTXO.self, HDTransaction.self) - print("✅ ModelContainer created") - } catch { - print("❌ Failed to create ModelContainer: \(error)") - throw error - } - } - - // Note: TransactionService is created in WalletService once SPV/UTXO context exists print("=== WalletManager.init SUCCESS ===") Task { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/HDTransaction.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/HDTransaction.swift index 5aaa0534e02..9e88284e5a2 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/HDTransaction.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/HDTransaction.swift @@ -91,177 +91,6 @@ public struct TransactionOutput: Codable { } } -// TransactionBuilder is now defined in TransactionBuilder.swift - -/* -public class TransactionBuilder { - private var inputs: [TransactionInput] = [] - private var outputs: [TransactionOutput] = [] - private let network: DashNetwork - private let feePerKB: UInt64 - - public init(network: DashNetwork, feePerKB: UInt64 = 1000) { - self.network = network - self.feePerKB = feePerKB - } - - // MARK: - Building Transaction - - public func addInput(utxo: HDUTXO, address: HDAddress) { - let input = TransactionInput( - txHash: utxo.txHash, - outputIndex: utxo.outputIndex, - script: Data(), // Will be filled during signing - amount: utxo.amount, - address: address.address - ) - inputs.append(input) - } - - public func addOutput(address: String, amount: UInt64) throws { - guard CoreSDKWrapper.shared.validateAddress(address, network: network) else { - throw TransactionError.invalidAddress - } - - let scriptPubKey = try createScriptPubKey(for: address) - let output = TransactionOutput( - amount: amount, - script: scriptPubKey, - address: address, - isChange: false - ) - outputs.append(output) - } - - public func addChangeOutput(address: String, amount: UInt64) throws { - guard CoreSDKWrapper.shared.validateAddress(address, network: network) else { - throw TransactionError.invalidAddress - } - - let scriptPubKey = try createScriptPubKey(for: address) - let output = TransactionOutput( - amount: amount, - script: scriptPubKey, - address: address, - isChange: true - ) - outputs.append(output) - } - - public func calculateFee() -> UInt64 { - // Estimate transaction size - let baseSize = 10 // Version (4) + locktime (4) + marker (2) - let inputSize = inputs.count * 148 // Approximate size per input with signature - let outputSize = outputs.count * 34 // Approximate size per output - let estimatedSize = baseSize + inputSize + outputSize - - // Calculate fee based on size - let fee = UInt64(estimatedSize) * feePerKB / 1000 - return max(fee, 1000) // Minimum fee of 1000 duffs - } - - public func build() throws -> RawTransaction { - guard !inputs.isEmpty else { - throw TransactionError.noInputs - } - - guard !outputs.isEmpty else { - throw TransactionError.noOutputs - } - - // Calculate total input and output amounts - let totalInput = inputs.compactMap { $0.amount }.reduce(0, +) - let totalOutput = outputs.reduce(0) { $0 + $1.amount } - let fee = calculateFee() - - guard totalInput >= totalOutput + fee else { - throw TransactionError.insufficientFunds - } - - // Create raw transaction - return RawTransaction( - version: 2, - inputs: inputs, - outputs: outputs, - lockTime: 0 - ) - } - - // MARK: - Signing - - public func sign(transaction: RawTransaction, with privateKeys: [String: Data]) throws -> Data { - // This should use actual transaction signing logic - // For now, return mock signed transaction - var signedInputs: [TransactionInput] = [] - - for (index, input) in transaction.inputs.enumerated() { - guard let address = input.address, - let privateKey = privateKeys[address] else { - throw TransactionError.missingPrivateKey - } - - // Create signature script - let signatureScript = try createSignatureScript( - for: transaction, - inputIndex: index, - privateKey: privateKey - ) - - let signedInput = TransactionInput( - txHash: input.txHash, - outputIndex: input.outputIndex, - script: signatureScript, - sequence: input.sequence, - amount: input.amount, - address: input.address - ) - signedInputs.append(signedInput) - } - - // Serialize signed transaction - let signedTx = RawTransaction( - version: transaction.version, - inputs: signedInputs, - outputs: transaction.outputs, - lockTime: transaction.lockTime - ) - - return try signedTx.serialize() - } - - // MARK: - Private Methods - - private func createScriptPubKey(for address: String) throws -> Data { - // This should create actual P2PKH script - // For now, return mock script - var script = Data() - script.append(0x76) // OP_DUP - script.append(0xa9) // OP_HASH160 - script.append(0x14) // Push 20 bytes - script.append(Data(repeating: 0, count: 20)) // Mock pubkey hash - script.append(0x88) // OP_EQUALVERIFY - script.append(0xac) // OP_CHECKSIG - return script - } - - private func createSignatureScript(for transaction: RawTransaction, inputIndex: Int, privateKey: Data) throws -> Data { - // This should create actual signature script - // For now, return mock script - let signature = CoreSDKWrapper.shared.signTransaction(Data(), with: privateKey) ?? Data() - let publicKey = CoreSDKWrapper.shared.derivePublicKey(from: privateKey) ?? Data() - - var script = Data() - script.append(UInt8(signature.count + 1)) // Signature length + hash type - script.append(signature) - script.append(0x01) // SIGHASH_ALL - script.append(UInt8(publicKey.count)) // Public key length - script.append(publicKey) - - return script - } -} -*/ - // MARK: - Raw Transaction public struct RawTransaction { @@ -341,6 +170,21 @@ public struct RawTransaction { } } -// TransactionError is now defined in TransactionBuilder.swift - -// Data hex extension is now defined in TransactionBuilder.swift +// Common hex initializer used by transaction code +extension Data { + init?(hex: String) { + let hex = hex.replacingOccurrences(of: " ", with: "") + guard hex.count % 2 == 0 else { return nil } + var data = Data(capacity: hex.count / 2) + var index = hex.startIndex + while index < hex.endIndex { + let next = hex.index(index, offsetBy: 2) + guard next <= hex.endIndex else { return nil } + let byteString = String(hex[index.. BuiltTransaction { - // Route to SDK transaction builder (stubbed for now) - guard walletManager.currentWallet != nil else { throw TransactionError.invalidState } - let builder = SwiftDashSDK.SDKTransactionBuilder(feePerKB: feePerKB) - // TODO: integrate coin selection + key derivation via SDK and add inputs/outputs - _ = builder // silence unused - throw TransactionError.notSupported("Transaction building is not yet wired to SwiftDashSDK") - } - - // MARK: - Transaction Broadcasting - - func broadcastTransaction(_ transaction: BuiltTransaction) async throws { - isBroadcasting = true - defer { isBroadcasting = false } - - do { - // Broadcast through SPV - // TODO: Implement broadcast with new SPV client - // try await spvClient.broadcastTransaction(transaction.rawTransaction) - throw TransactionError.broadcastFailed("SPV broadcast not yet implemented") - } catch { - lastError = error - throw TransactionError.broadcastFailed(error.localizedDescription) - } - } - - // MARK: - Transaction History - - public func loadTransactions() async { - isLoading = true - defer { isLoading = false } - - do { - let descriptor = FetchDescriptor( - sortBy: [SortDescriptor(\.timestamp, order: .reverse)] - ) - transactions = try modelContainer.mainContext.fetch(descriptor) - } catch { - print("Failed to load transactions: \(error)") - } - } - - public func processIncomingTransaction( - txid: String, - rawTx: Data, - blockHeight: Int?, - timestamp: Date = Date() - ) async throws { - // Check if transaction already exists - let existingDescriptor = FetchDescriptor( - predicate: #Predicate { $0.txHash == txid } - ) - - let existing = try modelContainer.mainContext.fetch(existingDescriptor) - if let existingTx = existing.first { - // Update existing transaction - existingTx.blockHeight = blockHeight - existingTx.confirmations = blockHeight != nil ? 1 : 0 - existingTx.isPending = blockHeight == nil - } else { - // Create new transaction - let hdTransaction = HDTransaction(txHash: txid, timestamp: timestamp) - hdTransaction.rawTransaction = rawTx - hdTransaction.blockHeight = blockHeight - hdTransaction.isPending = blockHeight == nil - hdTransaction.wallet = walletManager.currentWallet - - // TODO: Parse transaction to determine type and amount - // This would require deserializing the transaction and checking outputs - - modelContainer.mainContext.insert(hdTransaction) - } - - try modelContainer.mainContext.save() - await loadTransactions() - } - - // MARK: - SPV Integration - - public func syncWithSPV() async throws { - guard let wallet = walletManager.currentWallet else { - return - } - - // Watch all addresses - for account in wallet.accounts { - let allAddresses = account.externalAddresses + account.internalAddresses + - account.coinJoinAddresses + account.identityFundingAddresses - - for address in allAddresses { - // TODO: Implement watch address with new SPV client - // try await spvClient.watchAddress(address.address) - print("Would watch address: \(address.address)") - } - } - - // Start sync without blocking UI; inherit MainActor to avoid sending non-Sendable captures - let client = spvClient - Task(priority: .userInitiated) { - try? await client.startSync() - } - } - - // MARK: - Fee Estimation - - public func estimateFee(for amount: UInt64, account: HDAccount? = nil) throws -> UInt64 { - // Placeholder fixed fee until SDK fee estimator is wired - return 2000 - } -} diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift index a9bb65f6475..9a3ac21c6ed 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift @@ -286,12 +286,8 @@ struct CreateWalletView: View { throw WalletError.walletError("No network selected") } -<<<<<<< HEAD // Create exactly one wallet in the SDK; do not append network to label _ = try await walletService.createWallet( -======= - let _ = try await walletService.walletManager.createWallet( ->>>>>>> db8ce6d36 (refactoring wallet service optional fields into not optional) label: walletLabel, mnemonic: mnemonic, pin: walletPin, From 356df326b715266442f6edb348b44c0a7915fdf5 Mon Sep 17 00:00:00 2001 From: Borja Castellano Date: Mon, 9 Feb 2026 18:01:36 -0600 Subject: [PATCH 11/15] fixing wallet implementation architecture --- .../Core/Models/CoreTransaction.swift | 150 ------ .../SwiftDashSDK/Core/Models/CoreTypes.swift | 81 --- .../Core/Utils/ModelContainerHelper.swift | 4 - .../Core/Wallet/CoreWalletManager.swift | 499 +++--------------- .../Core/Wallet/HDTransaction.swift | 190 ------- .../SwiftDashSDK/Core/Wallet/HDWallet.swift | 271 +--------- .../SwiftDashSDK/KeyWallet/Wallet.swift | 18 +- .../Shared/UnifiedStateManager.swift | 1 - .../Core/Views/AccountDetailView.swift | 2 +- .../Core/Views/CoreContentView.swift | 54 +- .../Core/Views/CreateWalletView.swift | 3 - .../Core/Views/ReceiveAddressView.swift | 2 +- .../Core/Views/SendTransactionView.swift | 6 +- .../Core/Views/TransactionListView.swift | 15 +- .../Core/Views/WalletDetailView.swift | 86 ++- 15 files changed, 130 insertions(+), 1252 deletions(-) delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Core/Models/CoreTransaction.swift delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Core/Models/CoreTypes.swift delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/HDTransaction.swift diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Models/CoreTransaction.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Models/CoreTransaction.swift deleted file mode 100644 index 79a5f553ee1..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Models/CoreTransaction.swift +++ /dev/null @@ -1,150 +0,0 @@ -import Foundation - -public struct CoreTransaction: Identifiable, Equatable { - public let id: String // txid - public let amount: Int64 // positive for received, negative for sent - public let fee: UInt64 - public let timestamp: Date - public let blockHeight: Int64? - public let confirmations: Int - public let type: String // TransactionType is defined in HDTransaction.swift - public let memo: String? - public let inputs: [CoreTransactionInput] - public let outputs: [CoreTransactionOutput] - public let isInstantSend: Bool - public let isAssetLock: Bool - public let rawData: Data? - - public var isConfirmed: Bool { - confirmations >= 6 - } - - public var isPending: Bool { - confirmations == 0 - } - - public var formattedAmount: String { - let dash = Double(abs(amount)) / 100_000_000.0 - let sign = amount < 0 ? "-" : "+" - return "\(sign)\(String(format: "%.8f", dash)) DASH" - } - - public var formattedFee: String { - let dash = Double(fee) / 100_000_000.0 - return String(format: "%.8f DASH", dash) - } - - public init( - id: String, - amount: Int64, - fee: UInt64, - timestamp: Date, - blockHeight: Int64? = nil, - confirmations: Int = 0, - type: String, - memo: String? = nil, - inputs: [CoreTransactionInput] = [], - outputs: [CoreTransactionOutput] = [], - isInstantSend: Bool = false, - isAssetLock: Bool = false, - rawData: Data? = nil - ) { - self.id = id - self.amount = amount - self.fee = fee - self.timestamp = timestamp - self.blockHeight = blockHeight - self.confirmations = confirmations - self.type = type - self.memo = memo - self.inputs = inputs - self.outputs = outputs - self.isInstantSend = isInstantSend - self.isAssetLock = isAssetLock - self.rawData = rawData - } -} - -public struct CoreTransactionInput: Equatable { - public let previousTxid: String - public let previousOutputIndex: UInt32 - public let address: String? - public let amount: UInt64? - public let scriptSignature: Data - - public init( - previousTxid: String, - previousOutputIndex: UInt32, - address: String? = nil, - amount: UInt64? = nil, - scriptSignature: Data - ) { - self.previousTxid = previousTxid - self.previousOutputIndex = previousOutputIndex - self.address = address - self.amount = amount - self.scriptSignature = scriptSignature - } -} - -public struct CoreTransactionOutput: Equatable { - public let index: UInt32 - public let address: String - public let amount: UInt64 - public let scriptPubKey: Data - public let isChange: Bool - - public init( - index: UInt32, - address: String, - amount: UInt64, - scriptPubKey: Data, - isChange: Bool = false - ) { - self.index = index - self.address = address - self.amount = amount - self.scriptPubKey = scriptPubKey - self.isChange = isChange - } -} - -// Transaction builder for creating new transactions -public struct CoreTransactionBuilder { - public var inputs: [CoreTransactionInput] = [] - public var outputs: [CoreTransactionOutput] = [] - public var fee: UInt64 = 0 - public var isInstantSend: Bool = false - public var isAssetLock: Bool = false - public var memo: String? - - public init() {} - - public mutating func addInput(_ input: CoreTransactionInput) { - inputs.append(input) - } - - public mutating func addOutput(to address: String, amount: UInt64, isChange: Bool = false) { - let output = CoreTransactionOutput( - index: UInt32(outputs.count), - address: address, - amount: amount, - scriptPubKey: Data(), // Will be filled by SDK - isChange: isChange - ) - outputs.append(output) - } - - public var totalInputAmount: UInt64 { - inputs.compactMap { $0.amount }.reduce(0, +) - } - - public var totalOutputAmount: UInt64 { - outputs.reduce(0) { $0 + $1.amount } - } - - public var calculatedFee: UInt64 { - guard totalInputAmount >= totalOutputAmount else { return 0 } - return totalInputAmount - totalOutputAmount - } -} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Models/CoreTypes.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Models/CoreTypes.swift deleted file mode 100644 index a234ef487c9..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Models/CoreTypes.swift +++ /dev/null @@ -1,81 +0,0 @@ -import Foundation - -// Core SDK Types -// Note: These are now defined in their respective files: -// - DashNetwork is defined in WalletFFIBridge.swift -// - SPVClient is defined in SPVClient.swift -public typealias WalletFFI = Any - -// TransactionType is now defined in HDTransaction.swift - -// AddressType is now defined in HDWallet.swift - -// Sync state enum -public enum SyncState: String { - case notStarted = "not_started" - case syncing = "syncing" - case synced = "synced" - case error = "error" - - var displayName: String { - switch self { - case .notStarted: return "Not Started" - case .syncing: return "Syncing" - case .synced: return "Synced" - case .error: return "Error" - } - } -} - -// Watch status for addresses -public enum WatchStatus: String { - case active = "active" - case inactive = "inactive" - case error = "error" - - var displayName: String { - switch self { - case .active: return "Watching" - case .inactive: return "Not Watching" - case .error: return "Error" - } - } -} - -// InstantLock result -public struct InstantLock { - public let txid: String - public let isConfirmed: Bool - public let signature: Data? - public let confirmationTime: Date? - - public init(txid: String, isConfirmed: Bool, signature: Data? = nil, confirmationTime: Date? = nil) { - self.txid = txid - self.isConfirmed = isConfirmed - self.signature = signature - self.confirmationTime = confirmationTime - } -} - -// WalletError is now defined in WalletManager.swift - -// AssetLock errors -public enum AssetLockError: LocalizedError { - case insufficientBalance - case assetLockGenerationFailed - case instantLockTimeout - case broadcastFailed(String) - - public var errorDescription: String? { - switch self { - case .insufficientBalance: - return "Insufficient balance to create asset lock" - case .assetLockGenerationFailed: - return "Failed to generate asset lock transaction" - case .instantLockTimeout: - return "Timed out waiting for InstantLock confirmation" - case .broadcastFailed(let reason): - return "Failed to broadcast transaction: \(reason)" - } - } -} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Utils/ModelContainerHelper.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Utils/ModelContainerHelper.swift index a324a2fe00a..fb2a68fbbbe 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Utils/ModelContainerHelper.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Utils/ModelContainerHelper.swift @@ -6,10 +6,6 @@ public struct ModelContainerHelper { let schema = Schema([ // Core models HDWallet.self, - HDAddress.self, - HDTransaction.self, - HDUTXO.self, - HDWatchedAddress.self, // Platform models PersistentIdentity.self, diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift index 9e59bf7f065..be552f3ea2c 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift @@ -12,8 +12,6 @@ import DashSDKFFI @MainActor public class CoreWalletManager: ObservableObject { @Published public private(set) var wallets: [HDWallet] = [] - @Published public private(set) var currentWallet: HDWallet? - @Published public private(set) var isLoading = false @Published public private(set) var error: WalletError? // SDK wallet manager - this is the real wallet manager from the SDK @@ -21,10 +19,7 @@ public class CoreWalletManager: ObservableObject { private let modelContainer: ModelContainer private let storage = WalletStorage() - /// Initialize with an SDK wallet manager - /// - Parameters: - /// - sdkWalletManager: The SDK wallet manager from SwiftDashSDK - /// - modelContainer: SwiftData model container for persistence + /// Initialize with a valid SPVClient instance init(spvClient: SPVClient, modelContainer: ModelContainer) throws { print("=== WalletManager.init START ===") @@ -39,29 +34,13 @@ public class CoreWalletManager: ObservableObject { } // MARK: - Wallet Management - public func createWallet(label: String, mnemonic: String? = nil, pin: String, isImport: Bool = false) async throws -> HDWallet { + public func createWallet(label: String, mnemonic: String, pin: String, isImport: Bool = false) async throws -> HDWallet { print("WalletManager.createWallet called") - isLoading = true - defer { isLoading = false } - - // Generate or validate mnemonic using SDK - let finalMnemonic: String - if let mnemonic = mnemonic { - print("Validating provided mnemonic...") - guard SwiftDashSDK.Mnemonic.validate(mnemonic) else { - print("Mnemonic validation failed") - throw WalletError.invalidMnemonic - } - finalMnemonic = mnemonic - } else { - print("Generating new mnemonic...") - do { - finalMnemonic = try SwiftDashSDK.Mnemonic.generate(wordCount: 12) - // Do not log mnemonic to console - } catch { - print("Failed to generate mnemonic: \(error)") - throw WalletError.seedGenerationFailed - } + + print("Validating provided mnemonic...") + guard SwiftDashSDK.Mnemonic.validate(mnemonic) else { + print("Mnemonic validation failed") + throw WalletError.invalidMnemonic } // Add wallet through SDK (with bitfield networks) and capture serialized bytes for persistence @@ -70,13 +49,11 @@ public class CoreWalletManager: ObservableObject { do { // Calculate birthHeight based on wallet type // For imported wallets: use 730k for mainnet, 0 for test/devnets (need to sync from genesis) - // For new wallets: use 0 to signal "use latest checkpoint" (FFI interprets 0 as None) let birthHeight: UInt32 if isImport { // Imported wallet should sync from a reasonable historical point - birthHeight = sdkWalletManager.network == .mainnet ? 730_000 : 1 // Use 1 instead of 0 to avoid "latest checkpoint" interpretation + birthHeight = sdkWalletManager.network == .mainnet ? 730_000 : 0 } else { - // New wallet: pass 0 to use latest checkpoint (FFI converts 0 -> None -> latest) birthHeight = 0 } @@ -84,7 +61,7 @@ public class CoreWalletManager: ObservableObject { // Add wallet using SDK's WalletManager with combined network bitfield and serialize let result = try sdkWalletManager.addWalletAndSerialize( - mnemonic: finalMnemonic, + mnemonic: mnemonic, passphrase: nil, birthHeight: birthHeight, accountOptions: .default, @@ -101,39 +78,33 @@ public class CoreWalletManager: ObservableObject { } // Create HDWallet model for SwiftUI - let appNetwork = AppNetwork(network: sdkWalletManager.network) - let wallet = HDWallet(label: label, network: appNetwork, isImported: isImport) - wallet.walletId = walletId - - // Persist serialized wallet bytes for restoration on next launch - wallet.serializedWalletBytes = serializedBytes - - // Store encrypted seed (if needed for UI purposes) + let network = AppNetwork(network: sdkWalletManager.network) + let wallet = HDWallet(walletId: walletId, serializedWalletBytes: serializedBytes, label: label, network: network, isImported: isImport) + do { - let seed = try SwiftDashSDK.Mnemonic.toSeed(mnemonic: finalMnemonic) + let seed = try SwiftDashSDK.Mnemonic.toSeed(mnemonic: mnemonic) let encryptedSeed = try storage.storeSeed(seed, pin: pin) - wallet.encryptedSeed = encryptedSeed + // TODO: Disabled while refactoring wallet.encryptedSeed = encryptedSeed } catch { print("Failed to store seed: \(error)") // Continue anyway - wallet is already created } - - // Insert wallet into context + + // Insert wallet into context ans save it modelContainer.mainContext.insert(wallet) - - // Create default account model - _ = wallet.createAccount(at: 0) - - // Sync complete wallet state from Rust managed info - try await syncWalletFromManagedInfo(for: wallet) - - // Save to database try modelContainer.mainContext.save() + + return wallet + } + + public func deleteWallet(_ wallet: HDWallet) async throws { + let walletId = wallet.id - await loadWallets() - currentWallet = wallet + wallets.removeAll(where: { $0.id == walletId }) - return wallet + // Now safe to delete from SwiftData (cascade will delete accounts/addresses) + modelContainer.mainContext.delete(wallet) + try modelContainer.mainContext.save() } func importWallet(label: String, network: AppNetwork, mnemonic: String, pin: String) async throws -> HDWallet { @@ -142,66 +113,14 @@ public class CoreWalletManager: ObservableObject { try modelContainer.mainContext.save() return wallet } - - /// Restore a wallet from serialized bytes via SDK - public func restoreWalletFromBytes(_ walletBytes: Data) throws -> Data { - try sdkWalletManager.importWallet(from: walletBytes) - } - - /// Sync wallet data using SwiftDashSDK wrappers (no direct FFI in app) - private func syncWalletFromManagedInfo(for wallet: HDWallet) async throws { - guard let walletId = wallet.walletId else { throw WalletError.walletError("Wallet ID not available") } - let collection = try sdkWalletManager.getManagedAccountCollection(walletId: walletId) - - for account in wallet.accounts { - if let managed = collection.getBIP44Account(at: account.accountNumber) { - if let bal = try? managed.getBalance() { - account.confirmedBalance = bal.confirmed - account.unconfirmedBalance = bal.unconfirmed - } - if let pool = managed.getExternalAddressPool(), let infos = try? pool.getAddresses(from: 0, to: 20) { - account.externalAddresses.removeAll() - for info in infos { - let hd = HDAddress(address: info.address, index: info.index, derivationPath: info.path, addressType: .external, account: account) - hd.isUsed = info.used - modelContainer.mainContext.insert(hd) - account.externalAddresses.append(hd) - } - account.externalAddressIndex = UInt32(infos.count) - } - if let pool = managed.getInternalAddressPool(), let infos = try? pool.getAddresses(from: 0, to: 10) { - account.internalAddresses.removeAll() - for info in infos { - let hd = HDAddress(address: info.address, index: info.index, derivationPath: info.path, addressType: .internal, account: account) - hd.isUsed = info.used - modelContainer.mainContext.insert(hd) - account.internalAddresses.append(hd) - } - account.internalAddressIndex = UInt32(infos.count) - } - } - } - } - - // Removed: replaced by syncAccountAddresses(using SDK) - - public func unlockWallet(with pin: String) async throws -> Data { - return try storage.retrieveSeed(pin: pin) - } - + public func decryptSeed(_ encryptedSeed: Data?) -> Data? { // This method is used internally by other services // In a real implementation, this would decrypt using the current PIN // For now, return nil to indicate manual unlock is needed return nil } - - /// Get wallet IDs via SDK wrapper - func getWalletIds() throws -> [Data] { try sdkWalletManager.getWalletIds() } - - /// Get wallet balance via SDK wrapper - func getWalletBalance(walletId: Data) throws -> (confirmed: UInt64, unconfirmed: UInt64) { try sdkWalletManager.getWalletBalance(walletId: walletId) } - + public func changeWalletPIN(currentPIN: String, newPIN: String) async throws { // Retrieve seed with current PIN let seed = try storage.retrieveSeed(pin: currentPIN) @@ -221,65 +140,18 @@ public class CoreWalletManager: ObservableObject { public func unlockWithBiometric() async throws -> Data { return try storage.retrieveSeedWithBiometric() } - - func createWatchOnlyWallet(label: String, extendedPublicKey: String) async throws -> HDWallet { - isLoading = true - defer { isLoading = false } - - let appNetwork = AppNetwork(network: sdkWalletManager.network) - let wallet = HDWallet(label: label, network: appNetwork, isWatchOnly: true) - - // Create account with extended public key - let account = wallet.createAccount(at: 0) - account.extendedPublicKey = extendedPublicKey - - // Generate addresses from extended public key - try await generateWatchOnlyAddresses(for: account, count: 20, type: .external) - try await generateWatchOnlyAddresses(for: account, count: 10, type: .internal) - - // Save to database - modelContainer.mainContext.insert(wallet) - try modelContainer.mainContext.save() - - await loadWallets() - currentWallet = wallet - - return wallet - } - - public func deleteWallet(_ wallet: HDWallet) async throws { - let walletId = wallet.id - - // Update observable state FIRST to prevent UI from accessing deleted relationships - // This prevents the "Never access a full future backing data" crash - if currentWallet?.id == walletId { - currentWallet = wallets.first(where: { $0.id != walletId }) - } - wallets.removeAll(where: { $0.id == walletId }) - - // Now safe to delete from SwiftData (cascade will delete accounts/addresses) - modelContainer.mainContext.delete(wallet) - try modelContainer.mainContext.save() - - // Reload to ensure consistency - await loadWallets() - } - - // MARK: - Transaction Management + + // MARK: - Account Management /// Get transactions for a wallet /// - Parameters: /// - wallet: The wallet to get transactions for /// - accountIndex: The account index (default 0) /// - Returns: Array of wallet transactions - public func getTransactions(for wallet: HDWallet, accountIndex: UInt32 = 0) async throws -> [WalletTransaction] { - guard let walletId = wallet.walletId else { - throw WalletError.walletError("Wallet ID not available") - } - + public func getTransactions(for wallet: HDWallet, accountIndex: UInt32 = 0) -> [WalletTransaction] { // Get managed account - let managedAccount = try sdkWalletManager.getManagedAccount( - walletId: walletId, + let managedAccount = try! sdkWalletManager.getManagedAccount( + walletId: wallet.walletId, accountIndex: accountIndex, accountType: .standardBIP44 ) @@ -287,19 +159,26 @@ public class CoreWalletManager: ObservableObject { // Get current height (TODO: get from SPV client when available) let currentHeight: UInt32 = 0 - return try managedAccount.getTransactions(currentHeight: currentHeight) + return try! managedAccount.getTransactions(currentHeight: currentHeight) } - // MARK: - Account Management - + public func getBalance(for wallet: HDWallet, accountIndex: UInt32 = 0) -> Balance { + let managedAccount = try! sdkWalletManager.getManagedAccount( + walletId: wallet.walletId, + accountIndex: accountIndex, + accountType: .standardBIP44 + ) + + return try! managedAccount.getBalance() + } + /// Get detailed account information including xpub and addresses /// - Parameters: /// - wallet: The wallet containing the account /// - accountInfo: The account info to get details for /// - Returns: Detailed account information public func getAccountDetails(for wallet: HDWallet, accountInfo: AccountInfo) async throws -> AccountDetailInfo { - guard let walletId = wallet.walletId else { throw WalletError.walletError("Wallet ID not available") } - let collection = try sdkWalletManager.getManagedAccountCollection(walletId: walletId) + let collection = try sdkWalletManager.getManagedAccountCollection(walletId: wallet.walletId) // Resolve managed account from category and optional index var managed: ManagedAccount? @@ -372,9 +251,8 @@ public class CoreWalletManager: ObservableObject { /// Derive a private key as WIF from seed using a specific path (deferred to SDK) public func derivePrivateKeyAsWIF(for wallet: HDWallet, accountInfo: AccountInfo, addressIndex: UInt32) async throws -> String { - guard let walletId = wallet.walletId else { throw WalletError.walletError("Wallet ID not available") } // Obtain a non-owning Wallet wrapper from manager - guard let sdkWallet = try sdkWalletManager.getWallet(id: walletId) else { + guard let sdkWallet = try sdkWalletManager.getWallet(id: wallet.walletId) else { throw WalletError.walletError("Wallet not found in manager") } @@ -454,25 +332,7 @@ public class CoreWalletManager: ObservableObject { /// - wallet: The wallet model /// - Returns: Account information including balances and address counts public func getAccounts(for wallet: HDWallet) async throws -> [AccountInfo] { - guard let walletId = wallet.walletId else { throw WalletError.walletError("Wallet ID not available") } - let collection: ManagedAccountCollection - do { - collection = try sdkWalletManager.getManagedAccountCollection(walletId: walletId) - } catch let err as KeyWalletError { - // If the managed wallet info isn't found (e.g., after fresh start), try restoring from serialized bytes - if case .notFound = err, let bytes = wallet.serializedWalletBytes { - do { - let restoredId = try sdkWalletManager.importWallet(from: bytes) - if wallet.walletId != restoredId { wallet.walletId = restoredId } - // Retry once after import - collection = try sdkWalletManager.getManagedAccountCollection(walletId: wallet.walletId!) - } catch { - throw err - } - } else { - throw err - } - } + let collection = try sdkWalletManager.getManagedAccountCollection(walletId: wallet.walletId) var list: [AccountInfo] = [] func counts(_ m: ManagedAccount) -> (Int, Int) { @@ -553,259 +413,42 @@ public class CoreWalletManager: ObservableObject { } return list } - - public func createAccount(in wallet: HDWallet) async throws -> HDAccount { - guard !wallet.isWatchOnly else { - throw WalletError.watchOnlyWallet - } - - // Note: The FFI wallet manager handles account creation internally - // We're just creating UI models here to track them - let accountIndex = UInt32(wallet.accounts.count) - let account = wallet.createAccount(at: accountIndex) - - // Sync complete wallet state from Rust managed info - try await syncWalletFromManagedInfo(for: wallet) - - try modelContainer.mainContext.save() - - return account - } - - // MARK: - Address Management - - func generateAddresses(for account: HDAccount, count: Int, type: AddressType) async throws { - // Refresh address lists from SDK-managed pools (SDK maintains state) - guard let wallet = account.wallet else { throw WalletError.walletError("No wallet for account") } - try await syncWalletFromManagedInfo(for: wallet) - } - - private func generateWatchOnlyAddresses(for account: HDAccount, count: Int, type: AddressType) async throws { - // For watch-only wallets, we need to derive addresses from extended public key - // This would require implementing public key derivation - // For now, throw an error as this requires additional cryptographic operations - throw WalletError.notImplemented("Watch-only address generation") - } - - func getUnusedAddress(for account: HDAccount, type: AddressType = .external) async throws -> HDAddress { - let addresses: [HDAddress] - switch type { - case .external: - addresses = account.externalAddresses - case .internal: - addresses = account.internalAddresses - case .coinJoin: - addresses = account.coinJoinAddresses - case .identity: - addresses = account.identityFundingAddresses - } - - // Find first unused address - if let unusedAddress = addresses.first(where: { !$0.isUsed }) { - return unusedAddress - } - - // Generate new addresses if all are used - try await generateAddresses(for: account, count: 10, type: type) - - // Return the first newly generated address - guard let newAddress = addresses.first(where: { !$0.isUsed }) else { - throw WalletError.addressGenerationFailed - } - - return newAddress - } - - // MARK: - Balance Management - - func updateBalance(for account: HDAccount) async { - guard let wallet = account.wallet, - let walletId = wallet.walletId else { - return - } - - // Get balance via SDK wrappers - do { - let collection = try sdkWalletManager.getManagedAccountCollection(walletId: walletId) - if let managed = collection.getBIP44Account(at: account.accountNumber) { - if let bal = try? managed.getBalance() { - account.confirmedBalance = bal.confirmed - account.unconfirmedBalance = bal.unconfirmed - try? modelContainer.mainContext.save() - } - } - } catch { - print("Failed to update balance: \(error)") - } - } - - /// Sync all wallet state from Rust WalletManager to SwiftData - /// This should be called whenever the Rust wallet state changes (e.g., after processing a transaction) - func syncWalletStateFromRust(for wallet: HDWallet) async { - guard let walletId = wallet.walletId else { return } - - do { - let collection = try sdkWalletManager.getManagedAccountCollection(walletId: walletId) - - // Sync all accounts - for account in wallet.accounts { - if let managed = collection.getBIP44Account(at: account.accountNumber) { - // Sync balance from account - if let bal = try? managed.getBalance() { - account.confirmedBalance = bal.confirmed - account.unconfirmedBalance = bal.unconfirmed - } - - // Sync addresses (update isUsed flags and add new addresses) - // Use 0 to 0 to get all addresses from the pool - if let externalPool = managed.getExternalAddressPool() { - if let infos = try? externalPool.getAddresses(from: 0, to: 0) { - // Update existing addresses and add new ones if needed - for info in infos { - if let existingAddr = account.externalAddresses.first(where: { $0.address == info.address }) { - existingAddr.isUsed = info.used - } else { - // New address discovered - add it - let hd = HDAddress(address: info.address, index: info.index, derivationPath: info.path, addressType: .external, account: account) - hd.isUsed = info.used - modelContainer.mainContext.insert(hd) - account.externalAddresses.append(hd) - } - } - } - } - - if let internalPool = managed.getInternalAddressPool() { - if let infos = try? internalPool.getAddresses(from: 0, to: 0) { - for info in infos { - if let existingAddr = account.internalAddresses.first(where: { $0.address == info.address }) { - existingAddr.isUsed = info.used - } else { - let hd = HDAddress(address: info.address, index: info.index, derivationPath: info.path, addressType: .internal, account: account) - hd.isUsed = info.used - modelContainer.mainContext.insert(hd) - account.internalAddresses.append(hd) - } - } - } - } - } - } - - // Save all changes to SwiftData - try modelContainer.mainContext.save() - } catch { - print("❌ [WalletManager] Failed to sync wallet state: \(error)") - } - } - - // MARK: - Public Utility Methods - - func reloadWallets() async { - await loadWallets() - } - - /// Remove a wallet from the observable arrays without touching SwiftData - /// Call this BEFORE deleting from SwiftData to prevent UI from accessing deleted relationships - public func removeWalletFromObservableState(_ wallet: HDWallet) { - let walletId = wallet.id - - // Update observable state to prevent UI from accessing deleted relationships - if currentWallet?.id == walletId { - currentWallet = wallets.first(where: { $0.id != walletId }) - } - wallets.removeAll(where: { $0.id == walletId }) - } - + // MARK: - Private Methods private func loadWallets() async { + var wallets: [HDWallet] = [] do { let descriptor = FetchDescriptor(sortBy: [SortDescriptor(\.createdAt)]) wallets = try modelContainer.mainContext.fetch(descriptor) - - // Restore each wallet to the FFI wallet manager - for wallet in wallets { - if let walletBytes = wallet.serializedWalletBytes { - do { - // Restore wallet to FFI and update the wallet ID - let restoredWalletId = try restoreWalletFromBytes(walletBytes) - - // Update wallet ID if it changed (shouldn't happen, but good to verify) - if wallet.walletId != restoredWalletId { - print("Warning: Wallet ID changed during restoration. Old: \(wallet.walletId?.hexString ?? "nil"), New: \(restoredWalletId.hexString)") - wallet.walletId = restoredWalletId - } - - print("Successfully restored wallet '\(wallet.label)' to FFI wallet manager") - } catch { - // Handle wallet format migration errors - // The wallet serialization format changed from multi-network to single-network. - // Old wallet bytes cannot be deserialized with the new format. - let errorString = String(describing: error) - let isFormatMigrationError = errorString.contains("UnexpectedVariant") || - errorString.contains("serialization") || - errorString.contains("deserialize") || - errorString.contains("Network") - - if isFormatMigrationError { - print("⚠️ Wallet '\(wallet.label)' has incompatible serialization format (likely from old multi-network format).") - print(" Clearing invalid serialized bytes. Please delete and re-import this wallet using your mnemonic.") - print(" Error details: \(errorString)") - - // Clear the invalid serialized bytes so future loads don't keep failing - wallet.serializedWalletBytes = nil - // Mark wallet as needing recreation so UI can indicate this to user - wallet.needsRecreation = true - } else { - print("Failed to restore wallet '\(wallet.label)': \(error)") - } - // Continue loading other wallets even if one fails - } - } else { - print("Warning: Wallet '\(wallet.label)' has no serialized bytes - cannot restore to FFI") - } - } - - if currentWallet == nil, let firstWallet = wallets.first { - currentWallet = firstWallet - } - - // Save any wallet ID updates - try? modelContainer.mainContext.save() } catch { self.error = WalletError.databaseError(error.localizedDescription) + return } - } -} - - -// MARK: - Keychain Wrapper - -private class KeychainWrapper { - func set(_ data: Data, forKey key: String) { - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrAccount as String: key, - kSecValueData as String: data - ] - - SecItemDelete(query as CFDictionary) - SecItemAdd(query as CFDictionary, nil) - } + + // Try to import each wallet into the FFI wallet manager + // If it succeeds, we store the HDWallet for later querying. If it fails, + // we log the error and remove that wallet from the database. + for wallet in wallets { + do { + let restoredWalletId = try sdkWalletManager.importWallet(from: wallet.serializedWalletBytes) - func data(forKey key: String) -> Data? { - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrAccount as String: key, - kSecReturnData as String: true - ] + // Update wallet ID if it changed (shouldn't happen, but good to verify) + if wallet.walletId != restoredWalletId { + print("Warning: Wallet ID changed during restoration. Old: \(wallet.walletId.hexString ?? "nil"), New: \(restoredWalletId.hexString)") + wallet.walletId = restoredWalletId + } - var result: AnyObject? - let status = SecItemCopyMatching(query as CFDictionary, &result) + self.wallets.append(wallet) + + print("Successfully restored wallet '\(wallet.label)' to FFI wallet manager") + } catch { + let errorString = String(describing: error) + modelContainer.mainContext.delete(wallet) + } + } - guard status == errSecSuccess else { return nil } - return result as? Data + try? modelContainer.mainContext.save() } } diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/HDTransaction.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/HDTransaction.swift deleted file mode 100644 index 9e88284e5a2..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/HDTransaction.swift +++ /dev/null @@ -1,190 +0,0 @@ -import Foundation -import SwiftData - -// MARK: - HD Transaction - -@Model -public final class HDTransaction { - @Attribute(.unique) public var id: UUID - @Attribute(.unique) public var txHash: String - public var rawTransaction: Data? - public var blockHeight: Int? - public var blockHash: String? - public var timestamp: Date - public var confirmations: Int - public var size: Int - public var fee: UInt64 - public var type: String // "sent", "received", "self" - - // Inputs and outputs - public var inputsData: Data? // Serialized TransactionInput array - public var outputsData: Data? // Serialized TransactionOutput array - - // Relationships - @Relationship public var addresses: [HDAddress] = [] - @Relationship public var wallet: HDWallet? - - // Computed amount (positive for received, negative for sent) - public var amount: Int64 - - // Transaction status - public var isPending: Bool - public var isInstantSend: Bool - public var isChainLocked: Bool - - public init(txHash: String, timestamp: Date = Date()) { - self.id = UUID() - self.txHash = txHash - self.timestamp = timestamp - self.confirmations = 0 - self.size = 0 - self.fee = 0 - self.type = "received" - self.amount = 0 - self.isPending = true - self.isInstantSend = false - self.isChainLocked = false - } - - public var transactionType: TransactionType { - return TransactionType(rawValue: type) ?? .received - } -} - -public enum TransactionType: String { - case sent = "sent" - case received = "received" - case `self` = "self" -} - -// MARK: - Transaction Components - -public struct TransactionInput: Codable { - public let txHash: String - public let outputIndex: UInt32 - public let script: Data - public let sequence: UInt32 - public let amount: UInt64? - public let address: String? - - public init(txHash: String, outputIndex: UInt32, script: Data, sequence: UInt32 = 0xFFFFFFFF, amount: UInt64? = nil, address: String? = nil) { - self.txHash = txHash - self.outputIndex = outputIndex - self.script = script - self.sequence = sequence - self.amount = amount - self.address = address - } -} - -public struct TransactionOutput: Codable { - public let amount: UInt64 - public let script: Data - public let address: String? - public let isChange: Bool - - public init(amount: UInt64, script: Data, address: String? = nil, isChange: Bool = false) { - self.amount = amount - self.script = script - self.address = address - self.isChange = isChange - } -} - -// MARK: - Raw Transaction - -public struct RawTransaction { - public let version: UInt32 - public let inputs: [TransactionInput] - public let outputs: [TransactionOutput] - public let lockTime: UInt32 - - public func serialize() throws -> Data { - var data = Data() - - // Version - var versionLE = version.littleEndian - data.append(Data(bytes: &versionLE, count: 4)) - - // Input count (compact size) - data.append(compactSize(UInt64(inputs.count))) - - // Inputs - for input in inputs { - // Previous output - if let txHashData = Data(hex: input.txHash) { - data.append(contentsOf: txHashData.reversed()) // Little endian - } - var outputIndexLE = input.outputIndex.littleEndian - data.append(Data(bytes: &outputIndexLE, count: 4)) - - // Script - data.append(compactSize(UInt64(input.script.count))) - data.append(input.script) - - // Sequence - var sequenceLE = input.sequence.littleEndian - data.append(Data(bytes: &sequenceLE, count: 4)) - } - - // Output count - data.append(compactSize(UInt64(outputs.count))) - - // Outputs - for output in outputs { - // Amount - var amountLE = output.amount.littleEndian - data.append(Data(bytes: &amountLE, count: 8)) - - // Script - data.append(compactSize(UInt64(output.script.count))) - data.append(output.script) - } - - // Lock time - var lockTimeLE = lockTime.littleEndian - data.append(Data(bytes: &lockTimeLE, count: 4)) - - return data - } - - private func compactSize(_ value: UInt64) -> Data { - if value < 0xfd { - return Data([UInt8(value)]) - } else if value <= 0xffff { - var data = Data([0xfd]) - var valueLE = UInt16(value).littleEndian - data.append(Data(bytes: &valueLE, count: 2)) - return data - } else if value <= 0xffffffff { - var data = Data([0xfe]) - var valueLE = UInt32(value).littleEndian - data.append(Data(bytes: &valueLE, count: 4)) - return data - } else { - var data = Data([0xff]) - var valueLE = value.littleEndian - data.append(Data(bytes: &valueLE, count: 8)) - return data - } - } -} - -// Common hex initializer used by transaction code -extension Data { - init?(hex: String) { - let hex = hex.replacingOccurrences(of: " ", with: "") - guard hex.count % 2 == 0 else { return nil } - var data = Data(capacity: hex.count / 2) - var index = hex.startIndex - while index < hex.endIndex { - let next = hex.index(index, offsetBy: 2) - guard next <= hex.endIndex else { return nil } - let byteString = String(hex[index.. HDAccount { - let accountIndex = index ?? UInt32(accounts.count) - let account = HDAccount( - accountNumber: accountIndex, - label: "Account \(accountIndex)", - wallet: self - ) - accounts.append(account) - return account - } - - public func getAddress() -> HDAddress? { - guard let currentAccount = self.accounts.first else { - return nil - } - - return currentAccount.externalAddresses.last - } -} - -// MARK: - HD Account - -@Model -public final class HDAccount: HDWalletModels { - @Attribute(.unique) public var id: UUID - public var accountNumber: UInt32 - public var label: String - - // Extended public key for this account (watch-only capability) - public var extendedPublicKey: String? - - // Derivation paths - @Relationship(deleteRule: .cascade) public var externalAddresses: [HDAddress] = [] - @Relationship(deleteRule: .cascade) public var internalAddresses: [HDAddress] = [] - @Relationship(deleteRule: .cascade) public var coinJoinAddresses: [HDAddress] = [] - @Relationship(deleteRule: .cascade) public var identityFundingAddresses: [HDAddress] = [] - - // Indexes - public var externalAddressIndex: UInt32 - public var internalAddressIndex: UInt32 - public var coinJoinExternalIndex: UInt32 - public var coinJoinInternalIndex: UInt32 - public var identityFundingIndex: UInt32 - - // Balance tracking - public var confirmedBalance: UInt64 - public var unconfirmedBalance: UInt64 - - // Parent wallet - @Relationship(inverse: \HDWallet.accounts) public var wallet: HDWallet? - - public init(accountNumber: UInt32, label: String, wallet: HDWallet) { - self.id = UUID() - self.accountNumber = accountNumber - self.label = label - self.wallet = wallet - self.externalAddressIndex = 0 - self.internalAddressIndex = 0 - self.coinJoinExternalIndex = 0 - self.coinJoinInternalIndex = 0 - self.identityFundingIndex = 0 - self.confirmedBalance = 0 - self.unconfirmedBalance = 0 - } - - public var totalBalance: UInt64 { - return confirmedBalance + unconfirmedBalance - } - - // All addresses combined - public var addresses: [HDAddress] { - return externalAddresses + internalAddresses + coinJoinAddresses + identityFundingAddresses - } -} - -// MARK: - HD Address - -@Model -public final class HDAddress: HDWalletModels { - @Attribute(.unique) public var id: UUID - @Attribute(.unique) public var address: String - public var index: UInt32 - public var derivationPath: String - public var isUsed: Bool - public var balance: UInt64 - public var lastSeenTime: Date? - - // Address type - public var addressType: String // "external", "internal", "coinjoin", "identity" - - // Parent account - @Relationship public var account: HDAccount? - - // Associated transactions - @Relationship(deleteRule: .nullify) public var transactions: [HDTransaction] = [] - - // UTXOs - @Relationship(deleteRule: .cascade) public var utxos: [HDUTXO] = [] - - public init(address: String, index: UInt32, derivationPath: String, addressType: AddressType, account: HDAccount) { - self.id = UUID() - self.address = address - self.index = index - self.derivationPath = derivationPath - self.addressType = addressType.rawValue - self.isUsed = false - self.balance = 0 - self.account = account - } - - public var type: AddressType { - return AddressType(rawValue: addressType) ?? .external - } -} - -public enum AddressType: String { - case external = "external" - case `internal` = "internal" - case coinJoin = "coinjoin" - case identity = "identity" -} - -// MARK: - HD UTXO - -@Model -public final class HDUTXO: HDWalletModels { - @Attribute(.unique) public var id: UUID - public var txHash: String - public var outputIndex: UInt32 - public var amount: UInt64 - public var scriptPubKey: Data - public var blockHeight: Int? - public var isSpent: Bool - public var isCoinbase: Bool - - // Parent address - @Relationship(inverse: \HDAddress.utxos) public var address: HDAddress? - - // Spending transaction (if spent) - public var spendingTxHash: String? - public var spendingInputIndex: UInt32? - - public init(txHash: String, outputIndex: UInt32, amount: UInt64, scriptPubKey: Data, address: HDAddress) { - self.id = UUID() - self.txHash = txHash - self.outputIndex = outputIndex - self.amount = amount - self.scriptPubKey = scriptPubKey - self.address = address - self.isSpent = false - self.isCoinbase = false - } - - // Computed property to check if UTXO is confirmed - public var isConfirmed: Bool { - return blockHeight != nil - } - - // Alias for txHash - public var txid: String { - return txHash - } -} - -// MARK: - Watched Address (for import) - -@Model -public final class HDWatchedAddress: HDWalletModels { - @Attribute(.unique) public var id: UUID - @Attribute(.unique) public var address: String - public var label: String? - public var balance: UInt64 - public var lastSeenTime: Date? - - // Parent wallet - @Relationship public var wallet: HDWallet? - - // Associated transactions - @Relationship(deleteRule: .nullify) public var transactions: [HDTransaction] = [] - - public init(address: String, label: String? = nil, wallet: HDWallet) { - self.id = UUID() - self.address = address - self.label = label - self.balance = 0 - self.wallet = wallet - } -} - -// MARK: - Protocol for common functionality - -public protocol HDWalletModels: AnyObject { - var id: UUID { get set } } diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Wallet.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Wallet.swift index 9a2721047a8..5735a0cba68 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Wallet.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Wallet.swift @@ -378,23 +378,7 @@ public class Wallet { return count } - - // MARK: - Balance - - /// Get the wallet's total balance - public func getBalance() throws -> Balance { - // TODO: wallet_get_balance function no longer exists in FFI - throw KeyWalletError.notSupported("wallet_get_balance is not available in current FFI") - } - - /// Get balance for a specific account - /// - Parameter accountIndex: The account index - /// - Returns: The account balance - public func getAccountBalance(accountIndex: UInt32) throws -> Balance { - // TODO: wallet_get_account_balance function no longer exists in FFI - throw KeyWalletError.notSupported("wallet_get_account_balance is not available in current FFI") - } - + // MARK: - Key Derivation /// Get the extended public key for an account diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Shared/UnifiedStateManager.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Shared/UnifiedStateManager.swift index 9c8a61e10f5..38163bfaf14 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Shared/UnifiedStateManager.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Shared/UnifiedStateManager.swift @@ -9,7 +9,6 @@ public class UnifiedStateManager: ObservableObject { // Core wallet state @Published public var coreBalance = Balance() - @Published public var coreTransactions: [CoreTransaction] = [] // Platform state @Published public var platformIdentities: [DPPIdentity] = [] diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountDetailView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountDetailView.swift index dd5d9774023..c4db9837015 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountDetailView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountDetailView.swift @@ -126,7 +126,7 @@ struct AccountDetailView: View { Text("Network:") .foregroundColor(.secondary) Spacer() - Text(wallet.dashNetwork.rawValue.capitalized) + Text(wallet.network.rawValue.capitalized) .fontWeight(.medium) } } diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift index 61b0717cfb6..12b195d78c5 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CoreContentView.swift @@ -410,28 +410,23 @@ struct SyncProgressRow: View { struct WalletRowView: View { let wallet: HDWallet @EnvironmentObject var unifiedAppState: UnifiedAppState + @EnvironmentObject var walletService: WalletService private func getNetworksList() -> String { // Wallets are now single-network, just return the wallet's network - return wallet.dashNetwork.rawValue.capitalized + return wallet.network.rawValue.capitalized } var platformBalance: UInt64 { // Only sum balances of identities that belong to this specific wallet // and are on the same network - // For now, if wallet doesn't have a walletId (not yet initialized with FFI), - // don't show any platform balance - guard let walletId = wallet.walletId else { - return 0 - } - return unifiedAppState.platformState.identities .filter { identity in // Check if identity belongs to this wallet and is on the same network // Only count identities that have been explicitly associated with this wallet - identity.walletId == walletId && - identity.network == wallet.dashNetwork.rawValue + identity.walletId == wallet.walletId && + identity.network == wallet.network.rawValue } .reduce(0) { sum, identity in sum + identity.balance @@ -443,13 +438,6 @@ struct WalletRowView: View { HStack { Text(wallet.label) .font(.headline) - - Spacer() - - if wallet.syncProgress < 1.0 { - ProgressView(value: min(max(wallet.syncProgress, 0.0), 1.0)) - .frame(width: 50) - } } HStack { @@ -469,12 +457,13 @@ struct WalletRowView: View { VStack(alignment: .trailing, spacing: 2) { // Show wallet balance or "Empty" - if wallet.totalBalance == 0 { + let balance = walletService.walletManager.getBalance(for: wallet) + if balance.total == 0 { Text("Empty") .font(.caption) .foregroundColor(.secondary) } else { - Text(formatBalance(wallet.totalBalance)) + Text(balance.formattedTotal) .font(.subheadline) .fontWeight(.medium) } @@ -484,7 +473,7 @@ struct WalletRowView: View { HStack(spacing: 3) { Image(systemName: "p.circle.fill") .font(.system(size: 9)) - Text(formatBalance(platformBalance)) + Text(platformBalance.formatted()) } .font(.caption2) .foregroundColor(.blue) @@ -494,33 +483,6 @@ struct WalletRowView: View { } .padding(.vertical, 4) } - - private func formatBalance(_ amount: UInt64) -> String { - let dash = Double(amount) / 100_000_000.0 - - // Special case for zero - if dash == 0 { - return "0 DASH" - } - - // Format with up to 8 decimal places, removing trailing zeros - let formatter = NumberFormatter() - formatter.minimumFractionDigits = 0 - formatter.maximumFractionDigits = 8 - formatter.numberStyle = .decimal - formatter.groupingSeparator = "," - formatter.decimalSeparator = "." - - if let formatted = formatter.string(from: NSNumber(value: dash)) { - return "\(formatted) DASH" - } - - // Fallback formatting - let formatted = String(format: "%.8f", dash) - let trimmed = formatted.replacingOccurrences(of: "0+$", with: "", options: .regularExpression) - .replacingOccurrences(of: "\\.$", with: "", options: .regularExpression) - return "\(trimmed) DASH" - } } // MARK: - Formatting Helpers diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift index 9a3ac21c6ed..81dcadf457c 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift @@ -294,9 +294,6 @@ struct CreateWalletView: View { isImport: showImportOption ) - // Update wallet.networks bitfield to reflect all user selections - try? modelContext.save() - print("=== WALLET CREATION SUCCESS - Created 1 wallet for \(primaryNetwork.displayName) ===") await MainActor.run { diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/ReceiveAddressView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/ReceiveAddressView.swift index 58f50d23a8d..257ea936f8e 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/ReceiveAddressView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/ReceiveAddressView.swift @@ -12,7 +12,7 @@ struct ReceiveAddressView: View { var body: some View { NavigationStack { VStack(spacing: 24) { - let currentAddress = wallet.getAddress()?.address ?? "" + let currentAddress = "" // TODO: Need to return a valid address here // QR Code if let qrImage = generateQRCode(from: currentAddress) { diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift index f4c8dda7b91..2925ff228cd 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift @@ -22,7 +22,7 @@ struct SendTransactionView: View { !recipientAddress.isEmpty && amount != nil && amount! > 0 && - amount! <= wallet.confirmedBalance + amount! <= 99999 } var body: some View { @@ -48,14 +48,14 @@ struct SendTransactionView: View { HStack { Text("Available:") Spacer() - Text(formatBalance(wallet.confirmedBalance)) + Text(formatBalance(99999)) .font(.caption) .foregroundColor(.secondary) } } header: { Text("Amount") } footer: { - if let amount = amount, amount > wallet.confirmedBalance { + if let amount = amount, amount > 99999 { Text("Insufficient balance") .foregroundColor(.red) } diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/TransactionListView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/TransactionListView.swift index 43f0603f669..22c0d4610f3 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/TransactionListView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/TransactionListView.swift @@ -82,18 +82,11 @@ struct TransactionListView: View { isLoading = true defer { isLoading = false } - do { - // Get transactions from the wallet manager - let fetchedTransactions = try await walletService.walletManager.getTransactions(for: wallet) + // Get transactions from the wallet manager + let fetchedTransactions = walletService.walletManager.getTransactions(for: wallet) - await MainActor.run { - self.transactions = fetchedTransactions - } - } catch { - await MainActor.run { - self.errorMessage = error.localizedDescription - self.showError = true - } + await MainActor.run { + self.transactions = fetchedTransactions } } } diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift index b1d08570c48..f67ed205548 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift @@ -75,8 +75,10 @@ struct WalletDetailView: View { Spacer() - if wallet.transactionCount > 0 { - Text("\(wallet.transactionCount)") + let transactions = walletService.walletManager.getTransactions(for: wallet) + let transactionCount = transactions.count + if transactionCount > 0 { + Text("\(transactionCount)") .font(.caption) .foregroundColor(.secondary) .padding(.horizontal, 8) @@ -276,17 +278,15 @@ struct WalletInfoView: View { Text(wallet.createdAt, style: .date) .foregroundColor(.secondary) } - - if let walletId = wallet.walletId { - HStack { - Text("Wallet ID") - Spacer() - Text(walletId.toHexString()) - .font(.system(.footnote, design: .monospaced)) - .foregroundColor(.secondary) - .textSelection(.enabled) - .multilineTextAlignment(.trailing) - } + + HStack { + Text("Wallet ID") + Spacer() + Text(wallet.walletId.toHexString()) + .font(.system(.footnote, design: .monospaced)) + .foregroundColor(.secondary) + .textSelection(.enabled) + .multilineTextAlignment(.trailing) } if mainnetEnabled { @@ -370,7 +370,7 @@ struct WalletInfoView: View { private func loadNetworkStates() { // TODO: Probably not needed this way anymore? - switch wallet.dashNetwork { + switch wallet.network { case .mainnet: mainnetEnabled = true case .testnet: @@ -449,57 +449,32 @@ struct WalletInfoView: View { } private func deleteWallet() async { - isDeleting = true - defer { - Task { @MainActor in - isDeleting = false - } + // IMPORTANT: Dismiss views FIRST to prevent UI from accessing deleted relationships + // This prevents "Never access a full future backing data" crash + await MainActor.run { + dismiss() + onWalletDeleted() } - do { - // IMPORTANT: Dismiss views FIRST to prevent UI from accessing deleted relationships - // This prevents "Never access a full future backing data" crash - await MainActor.run { - dismiss() - onWalletDeleted() - } - - // Notify the wallet service (removes wallet from observable arrays) - walletService.walletManager.removeWalletFromObservableState(wallet) - - // Now safe to delete from Core Data (cascade will delete accounts/addresses) - modelContext.delete(wallet) - try modelContext.save() - - } catch { - await MainActor.run { - errorMessage = "Failed to delete wallet: \(error.localizedDescription)" - showError = true - } - } + try! await walletService.walletManager.deleteWallet(wallet) } } struct BalanceCardView: View { let wallet: HDWallet @EnvironmentObject var unifiedAppState: UnifiedAppState - + @EnvironmentObject var walletService: WalletService + var platformBalance: UInt64 { // Only sum balances of identities that belong to this specific wallet // and are on the same network - - // For now, if wallet doesn't have a walletId (not yet initialized with FFI), - // don't show any platform balance - guard let walletId = wallet.walletId else { - return 0 - } - + return unifiedAppState.platformState.identities .filter { identity in // Check if identity belongs to this wallet and is on the same network // Only count identities that have been explicitly associated with this wallet - identity.walletId == walletId && - identity.network == wallet.dashNetwork.rawValue + identity.walletId == wallet.walletId && + identity.network == wallet.network.rawValue } .reduce(0) { sum, identity in sum + identity.balance @@ -509,7 +484,8 @@ struct BalanceCardView: View { var body: some View { VStack(spacing: 12) { // Show main balance or "Empty Wallet" - if wallet.totalBalance == 0 { + let balance = walletService.walletManager.getBalance(for: wallet) + if balance.total == 0 { Text("Empty Wallet") .font(.system(size: 28, weight: .medium, design: .rounded)) .foregroundColor(.secondary) @@ -517,8 +493,8 @@ struct BalanceCardView: View { Text("Wallet Balance") .font(.subheadline) .foregroundColor(.secondary) - - Text(formatBalance(wallet.totalBalance)) + + Text(balance.formattedTotal) .font(.system(size: 36, weight: .bold, design: .rounded)) } @@ -528,8 +504,8 @@ struct BalanceCardView: View { Text("Incoming") .font(.caption) .foregroundColor(.secondary) - if wallet.unconfirmedBalance > 0 { - Text(formatBalance(wallet.unconfirmedBalance)) + if balance.unconfirmed > 0 { + Text(balance.formattedUnconfirmed) .font(.subheadline) .fontWeight(.medium) .foregroundColor(.orange) From cddaace3010753913698aeacc002e422e11f9c84 Mon Sep 17 00:00:00 2001 From: Borja Castellano Date: Wed, 11 Feb 2026 09:59:09 -0600 Subject: [PATCH 12/15] fixed double free issue by setting pointer to null afrter free --- .../Sources/SwiftDashSDK/Core/SPV/SPVClient.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift index bcae6f4749a..a33193153b2 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift @@ -12,7 +12,7 @@ import Foundation /// - Once stopped, the SPVClient cannot be restarted without creating a new instance, FFI limitation /// - The pointers are freed automatically but, to be able to create a new instance (with the same dataDir) /// without causing the initialization to fail, you must call SPVClient::destroy manually, this is because, -/// FFIDashSpvClient locks the dir manually to avoid concurrency corruption and its only possible to +/// FFIDashSpvClient locks the dir to avoid concurrency corruption and its only possible to /// ensure is unlocked by freeing the pointer, FFI limitation /// - Clearing the storage stops the SPVClient, a new one has to be created, FFI limitation class SPVClient: @unchecked Sendable { @@ -22,8 +22,8 @@ class SPVClient: @unchecked Sendable { private var walletEventsHandler: SPVWalletEventsHandler? // FFI handles - private let client: UnsafeMutablePointer - private let config: UnsafeMutablePointer + private var client: UnsafeMutablePointer? + private var config: UnsafeMutablePointer? // Sync tracking @@ -266,6 +266,9 @@ class SPVClient: @unchecked Sendable { func destroy() { dash_spv_ffi_client_destroy(client) dash_spv_ffi_config_destroy(config) + + client = nil + config = nil } // MARK: - Synchronization From e68ebd1b5c7bf02525659f5d83fa2ee21de1bbd7 Mon Sep 17 00:00:00 2001 From: Borja Castellano Date: Wed, 11 Feb 2026 11:06:28 -0600 Subject: [PATCH 13/15] adjusted the tests --- .../SwiftExampleAppTests/WalletTests/TransactionTests.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/WalletTests/TransactionTests.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/WalletTests/TransactionTests.swift index f9d88622451..c964f14c8fb 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/WalletTests/TransactionTests.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/WalletTests/TransactionTests.swift @@ -104,7 +104,6 @@ struct MockAddress: AddressProtocol { let address: String let derivationPath: String = "m/44'/5'/0'/0/0" let index: UInt32 = 0 - let type: AddressType = .external } // MARK: - Fee Calculator @@ -141,12 +140,8 @@ protocol AddressProtocol { var address: String { get } var derivationPath: String { get } var index: UInt32 { get } - var type: AddressType { get } } -extension HDUTXO: UTXOProtocol {} -extension HDAddress: AddressProtocol {} - // MARK: - Mock coin selection for testing (UTXOManager extension removed; type not in SDK) struct MockCoinSelection { let utxos: [any UTXOProtocol] From e4adc889f8ab1cee8d3c669e25d83544a2b53eb2 Mon Sep 17 00:00:00 2001 From: Borja Castellano Date: Wed, 11 Feb 2026 12:24:55 -0600 Subject: [PATCH 14/15] receive address view impleemnted --- .../Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift | 4 ++++ .../SwiftExampleApp/Core/Views/ReceiveAddressView.swift | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift index be552f3ea2c..918fd7e3d6c 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift @@ -172,6 +172,10 @@ public class CoreWalletManager: ObservableObject { return try! managedAccount.getBalance() } + public func getReceiveAddress(for wallet: HDWallet, accountIndex: UInt32 = 0) -> String { + return try! sdkWalletManager.getReceiveAddress(walletId: wallet.walletId, accountIndex: 0) + } + /// Get detailed account information including xpub and addresses /// - Parameters: /// - wallet: The wallet containing the account diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/ReceiveAddressView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/ReceiveAddressView.swift index 257ea936f8e..e4202258827 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/ReceiveAddressView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/ReceiveAddressView.swift @@ -12,7 +12,7 @@ struct ReceiveAddressView: View { var body: some View { NavigationStack { VStack(spacing: 24) { - let currentAddress = "" // TODO: Need to return a valid address here + let currentAddress = walletService.walletManager.getReceiveAddress(for: wallet) // QR Code if let qrImage = generateQRCode(from: currentAddress) { From 8574665b0ce3815473ed7d02a5c2a1b229d030dd Mon Sep 17 00:00:00 2001 From: Borja Castellano Date: Mon, 9 Mar 2026 16:33:11 -0500 Subject: [PATCH 15/15] sugestions solved --- .../Core/Services/WalletService.swift | 3 +- .../Core/Wallet/CoreWalletManager.swift | 8 +- .../Core/Views/AccountDetailView.swift | 7 - .../Core/Views/CreateWalletView.swift | 3 +- .../Core/Views/WalletDetailView.swift | 180 +----------------- .../SwiftExampleApp/UnifiedAppState.swift | 8 - 6 files changed, 7 insertions(+), 202 deletions(-) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift index 9d95995dfbd..da591c836b0 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift @@ -236,10 +236,11 @@ public class WalletService: ObservableObject { public func switchNetwork(to network: AppNetwork) async { guard network != self.network else { return } - self.network = network print("=== WalletService.switchNetwork START ===") print("Switching from \(self.network.rawValue) to \(network.rawValue)") + + self.network = network self.stopSync() diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift index 918fd7e3d6c..8eb89bd9cc2 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/CoreWalletManager.swift @@ -83,8 +83,7 @@ public class CoreWalletManager: ObservableObject { do { let seed = try SwiftDashSDK.Mnemonic.toSeed(mnemonic: mnemonic) - let encryptedSeed = try storage.storeSeed(seed, pin: pin) - // TODO: Disabled while refactoring wallet.encryptedSeed = encryptedSeed + _ = try storage.storeSeed(seed, pin: pin) } catch { print("Failed to store seed: \(error)") // Continue anyway - wallet is already created @@ -173,7 +172,7 @@ public class CoreWalletManager: ObservableObject { } public func getReceiveAddress(for wallet: HDWallet, accountIndex: UInt32 = 0) -> String { - return try! sdkWalletManager.getReceiveAddress(walletId: wallet.walletId, accountIndex: 0) + return try! sdkWalletManager.getReceiveAddress(walletId: wallet.walletId, accountIndex: accountIndex) } /// Get detailed account information including xpub and addresses @@ -439,7 +438,7 @@ public class CoreWalletManager: ObservableObject { // Update wallet ID if it changed (shouldn't happen, but good to verify) if wallet.walletId != restoredWalletId { - print("Warning: Wallet ID changed during restoration. Old: \(wallet.walletId.hexString ?? "nil"), New: \(restoredWalletId.hexString)") + print("Warning: Wallet ID changed during restoration. Old: \(wallet.walletId.hexString), New: \(restoredWalletId.hexString)") wallet.walletId = restoredWalletId } @@ -447,7 +446,6 @@ public class CoreWalletManager: ObservableObject { print("Successfully restored wallet '\(wallet.label)' to FFI wallet manager") } catch { - let errorString = String(describing: error) modelContainer.mainContext.delete(wallet) } } diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountDetailView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountDetailView.swift index c4db9837015..535005e1694 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountDetailView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountDetailView.swift @@ -627,13 +627,6 @@ struct AccountDetailView: View { errorMessage = nil do { -<<<<<<< HEAD - guard let walletManager = walletService.walletManager else { - throw WalletError.walletError("Wallet manager not available") - } - -======= ->>>>>>> db8ce6d36 (refactoring wallet service optional fields into not optional) // Get extended public key and other details let details = try await walletService.walletManager.getAccountDetails( for: wallet, diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift index 81dcadf457c..68bc0220e35 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift @@ -271,7 +271,6 @@ struct CreateWalletView: View { print("=== STARTING WALLET CREATION ===") let mnemonic = (showImportOption ? importMnemonic : mnemonic) - print("Has mnemonic: \(mnemonic)") print("PIN length: \(walletPin.count)") print("Import option enabled: \(showImportOption)") @@ -287,7 +286,7 @@ struct CreateWalletView: View { } // Create exactly one wallet in the SDK; do not append network to label - _ = try await walletService.createWallet( + _ = try await walletService.walletManager.createWallet( label: walletLabel, mnemonic: mnemonic, pin: walletPin, diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift index f67ed205548..117bdf2b29c 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift @@ -559,182 +559,4 @@ struct BalanceCardView: View { return String(format: "%.8f DASH", dash).replacingOccurrences(of: "0+$", with: "", options: .regularExpression).replacingOccurrences(of: "\\.$", with: "", options: .regularExpression) } -} - -// MARK: - Legacy Views (kept for reference) -// These views show transactions, addresses, and UTXOs directly -// They have been replaced by AccountListView which shows account-level information - -/* -struct TransactionListView: View { - let transactions: [HDTransaction] - - var body: some View { - if transactions.isEmpty { - ContentUnavailableView( - "No Transactions", - systemImage: "list.bullet.rectangle", - description: Text("Transactions will appear here") - ) - } else { - List(transactions.sorted(by: { $0.timestamp > $1.timestamp })) { transaction in - TransactionRowView(transaction: transaction) - } - .listStyle(.plain) - } - } -} - -struct TransactionRowView: View { - let transaction: HDTransaction - - var body: some View { - HStack { - Image(systemName: transaction.amount < 0 ? "arrow.up.circle" : "arrow.down.circle") - .font(.title2) - .foregroundColor(transaction.amount < 0 ? .red : .green) - - VStack(alignment: .leading, spacing: 4) { - Text(transaction.type.capitalized) - .font(.subheadline) - .fontWeight(.medium) - - Text(transaction.timestamp, style: .date) - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - VStack(alignment: .trailing, spacing: 4) { - Text(formatAmount(transaction.amount)) - .font(.subheadline) - .fontWeight(.medium) - .foregroundColor(transaction.amount < 0 ? .red : .green) - - if transaction.isPending { - Text("Pending") - .font(.caption) - .foregroundColor(.orange) - } else { - Text("\(transaction.confirmations) conf") - .font(.caption) - .foregroundColor(.secondary) - } - } - } - .padding(.vertical, 4) - } - - private func formatAmount(_ amount: Int64) -> String { - let dash = Double(abs(amount)) / 100_000_000.0 - let sign = amount < 0 ? "-" : "+" - return "\(sign)\(String(format: "%.8f", dash))" - } -} - -struct AddressListView: View { - let addresses: [HDAddress] - - var body: some View { - if addresses.isEmpty { - ContentUnavailableView( - "No Addresses", - systemImage: "qrcode", - description: Text("Addresses will appear here") - ) - } else { - List(addresses.sorted(by: { $0.index < $1.index })) { address in - AddressRowView(address: address) - } - .listStyle(.plain) - } - } -} - -struct AddressRowView: View { - let address: HDAddress - - var body: some View { - VStack(alignment: .leading, spacing: 4) { - HStack { - Text("Address #\(address.index)") - .font(.subheadline) - .fontWeight(.medium) - - Spacer() - - if address.isUsed { - Label("Used", systemImage: "checkmark.circle.fill") - .font(.caption) - .foregroundColor(.green) - } - } - - Text(address.address) - .font(.system(.caption, design: .monospaced)) - .foregroundColor(.secondary) - .lineLimit(1) - .truncationMode(.middle) - } - .padding(.vertical, 4) - } -} - -struct UTXOListView: View { - let utxos: [HDUTXO] - - var body: some View { - if utxos.isEmpty { - ContentUnavailableView( - "No UTXOs", - systemImage: "bitcoinsign.circle", - description: Text("Unspent outputs will appear here") - ) - } else { - List(utxos) { utxo in - UTXORowView(utxo: utxo) - } - .listStyle(.plain) - } - } -} - -struct UTXORowView: View { - let utxo: HDUTXO - - var body: some View { - VStack(alignment: .leading, spacing: 4) { - HStack { - Text(formatAmount(utxo.amount)) - .font(.subheadline) - .fontWeight(.medium) - - Spacer() - - if utxo.isConfirmed { - Label("Confirmed", systemImage: "checkmark.circle.fill") - .font(.caption) - .foregroundColor(.green) - } else { - Label("Unconfirmed", systemImage: "clock") - .font(.caption) - .foregroundColor(.orange) - } - } - - Text("\(utxo.txid):\(utxo.outputIndex)") - .font(.system(.caption, design: .monospaced)) - .foregroundColor(.secondary) - .lineLimit(1) - .truncationMode(.middle) - } - .padding(.vertical, 4) - } - - private func formatAmount(_ amount: UInt64) -> String { - let dash = Double(amount) / 100_000_000.0 - return String(format: "%.8f DASH", dash) - } -} -*/ +} \ No newline at end of file diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/UnifiedAppState.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/UnifiedAppState.swift index 693a2b342a9..2e498e2d5ce 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/UnifiedAppState.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/UnifiedAppState.swift @@ -53,15 +53,7 @@ class UnifiedAppState: ObservableObject { // Initialize services self.platformState = AppState() -<<<<<<< HEAD - - // Configure wallet service with the current network from platform state - self.walletService.configure(modelContainer: modelContainer, network: platformState.currentNetwork) - -======= self.walletService = WalletService(modelContainer: modelContainer, network: platformState.currentNetwork) - ->>>>>>> db8ce6d36 (refactoring wallet service optional fields into not optional) // Initialize unified state (will be updated with real SDKs during async init) self.unifiedState = UnifiedStateManager() }