diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift index df67f16b16..3f282c5ae4 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift @@ -3,11 +3,6 @@ import Foundation /// This class wraps the FFIDashSpvClient and provides a Swift interface for interacting with it /// -/// All dash_spv_ffi_* function calls are being wrapped in this class to avoid missusage -/// -/// The FFI is still a work in progress, that means that some functionalities are not yet -/// implemented or may change in the future, this wrap was made with that in mind. -/// /// Important limitations: /// - 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) @@ -18,11 +13,11 @@ import Foundation public class SPVClient: @unchecked Sendable { private var spvEventHandlers: SPVEventHandlers? - // FFI handles + // FFI handle private var client: UnsafeMutablePointer? - private var config: UnsafeMutablePointer? - // Sync tracking + // FFIClientConfig wrapper + private var config: SPVConfig fileprivate let swiftLoggingEnabled: Bool = { if let env = ProcessInfo.processInfo.environment["SPV_SWIFT_LOG"], env.lowercased() == "1" || env.lowercased() == "true" { @@ -31,122 +26,32 @@ public class SPVClient: @unchecked Sendable { return false }() - public init(network: Network = DashSDKNetwork(rawValue: 1), dataDir: String?, startHeight: UInt32, eventHandlers: SPVEventHandlers? = nil) throws { + /// Its recomended to avoid reusing the same config for multiple clients, + /// just in case the FFI layer stops cloning the config internally. + public init(config: consuming SPVConfig, eventHandlers: SPVEventHandlers? = nil) throws { if swiftLoggingEnabled { let level = (ProcessInfo.processInfo.environment["SPV_LOG"] ?? "off") print("[SPV][Log] Initialized SPV logging level=\(level)") } - // Create configuration based on network raw value - let configPtr: UnsafeMutablePointer? = { - switch network.rawValue { - case 0: - return dash_spv_ffi_config_mainnet() - case 1: - return dash_spv_ffi_config_testnet() - case 2: - // Regtest (local Docker) - return dash_spv_ffi_config_new(FFINetwork(rawValue: 2)) - case 3: - // Map devnet to custom FFINetwork value 3 - return dash_spv_ffi_config_new(FFINetwork(rawValue: 3)) - default: - return dash_spv_ffi_config_testnet() - } - }() - - guard let configPtr = configPtr else { - throw SPVError.configurationFailed - } - - // If requested, prefer local core peers (defaults to 127.0.0.1 with network default port) - let useLocalCore = UserDefaults.standard.bool(forKey: "useLocalhostCore") - || UserDefaults.standard.bool(forKey: "useDockerSetup") - // Only restrict to configured peers when using local core, if not, allow DNS discovery - let restrictToConfiguredPeers = useLocalCore - if useLocalCore { - let peers = SPVClient.readLocalCorePeers() - if swiftLoggingEnabled { - print("[SPV][Config] Use Local Core enabled; peers=\(peers.joined(separator: ", "))") - } - // Clear default peers before adding custom Docker peers - dash_spv_ffi_config_clear_peers(configPtr) - // Add peers via FFI (supports "ip:port" or bare IP for network-default port) - for addr in peers { - addr.withCString { cstr in - let rc = dash_spv_ffi_config_add_peer(configPtr, cstr) - if rc != 0 { - print("[SPV][Config] add_peer failed for \(addr): \(SPVClient.getLastDashFFIError())") - } - } - } - } - - // Apply restrict-to-configured-peers if requested - if restrictToConfiguredPeers { - if swiftLoggingEnabled { print("[SPV][Config] Enabling restrict-to-configured-peers mode") } - _ = dash_spv_ffi_config_set_restrict_to_configured_peers(configPtr, true) - } - - // Set data directory if provided - if let dataDir = dataDir { - let result = dash_spv_ffi_config_set_data_dir(configPtr, dataDir) - if result != 0 { - throw SPVError.configurationFailed - } - } - - // Enable mempool tracking and ensure detailed events are available - dash_spv_ffi_config_set_mempool_tracking(configPtr, true) - dash_spv_ffi_config_set_mempool_strategy(configPtr, FFIMempoolStrategy(rawValue: 0)) // FetchAll - _ = dash_spv_ffi_config_set_fetch_mempool_transactions(configPtr, true) - - // Set user agent to include SwiftDashSDK version from the framework bundle - do { - let bundle = Bundle(for: SPVClient.self) - let version = (bundle.infoDictionary?["CFBundleShortVersionString"] as? String) - ?? (bundle.infoDictionary?["CFBundleVersion"] as? String) - ?? "dev" - let ua = "SwiftDashSDK/\(version)" - // Always print what we're about to set for easier debugging - print("Setting user agent to \(ua)") - let rc = dash_spv_ffi_config_set_user_agent(configPtr, ua) - if rc != 0 { - print("[SPV][Config] Failed to set user agent (rc=\(rc)): \(SPVClient.getLastDashFFIError())") - throw SPVError.configurationFailed - } - if swiftLoggingEnabled { print("[SPV][Config] User-Agent=\(ua)") } - } - - _ = dash_spv_ffi_config_set_start_from_height(configPtr, startHeight) + self.config = config // Store event handlers so the pointers remain valid self.spvEventHandlers = eventHandlers // Create client with event callbacks let callbacks = eventHandlers?.intoFFIEventCallbacks() ?? FFIEventCallbacks() - let client = dash_spv_ffi_client_new(configPtr, callbacks) + let client = dash_spv_ffi_client_new(self.config.config, callbacks) guard let client = client else { print("[SPV][Init] Failed to create client: \(SPVClient.getLastDashFFIError())") throw SPVError.initializationFailed } self.client = client - config = configPtr } deinit { - self.destroy() - } - - private static func readLocalCorePeers() -> [String] { - // If no override is set, default to dashmate Docker Core P2P port - let raw = UserDefaults.standard.string(forKey: "corePeerAddresses")?.trimmingCharacters(in: .whitespacesAndNewlines) - let list = (raw?.isEmpty == false ? raw! : "127.0.0.1:20001") - return list - .split(separator: ",") - .map { $0.trimmingCharacters(in: .whitespaces) } - .filter { !$0.isEmpty } + destroy() } public func getSyncProgress() -> SPVSyncProgress { @@ -165,13 +70,9 @@ public class SPVClient: @unchecked Sendable { return String(cString: errorMsg) } - /// Enable/disable masternode sync. If the client is running, apply the update immediately. - public func setMasternodeSyncEnabled(_ enabled: Bool) throws { - var rc = dash_spv_ffi_config_set_masternode_sync_enabled(config, enabled) - if rc != 0 { throw SPVError.configurationFailed } - - rc = dash_spv_ffi_client_update_config(client, config) - if rc != 0 { throw SPVError.configurationFailed } + public func updateConfig(_ config: consuming SPVConfig) { + self.config = config; + dash_spv_ffi_client_update_config(client, self.config.config) } /// Clear all persisted SPV storage (headers, filters, metadata, sync state). @@ -189,10 +90,8 @@ public class SPVClient: @unchecked Sendable { public func destroy() { dash_spv_ffi_client_destroy(client) - dash_spv_ffi_config_destroy(config) client = nil - config = nil } // MARK: - Broadcast Transactions @@ -253,7 +152,6 @@ public class SPVClient: @unchecked Sendable { public enum SPVError: LocalizedError { case notInitialized case alreadyInitialized - case configurationFailed case initializationFailed case startFailed(String) case alreadySyncing @@ -267,8 +165,6 @@ public enum SPVError: LocalizedError { return "SPV client is not initialized" case .alreadyInitialized: return "SPV client is already initialized" - case .configurationFailed: - return "Failed to configure SPV client" case .initializationFailed: return "Failed to initialize SPV client" case let .startFailed(reason): diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVConfig.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVConfig.swift new file mode 100644 index 0000000000..7ead2db48d --- /dev/null +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVConfig.swift @@ -0,0 +1,96 @@ +import Foundation +import DashSDKFFI + +public class SPVConfig: @unchecked Sendable { + internal var config: UnsafeMutablePointer + + public init(_ network: Network) { + config = { + switch network.rawValue { + case 0: + return dash_spv_ffi_config_mainnet() + case 1: + return dash_spv_ffi_config_testnet() + case 2: + // Regtest (local Docker) + return dash_spv_ffi_config_new(FFINetwork(rawValue: 2)) + case 3: + // Map devnet to custom FFINetwork value 3 + return dash_spv_ffi_config_new(FFINetwork(rawValue: 3)) + default: + return dash_spv_ffi_config_testnet() + } + }() + + // If requested, prefer local core peers (defaults to 127.0.0.1 with network default port) + let useLocalCore = UserDefaults.standard.bool(forKey: "useLocalhostCore") + || UserDefaults.standard.bool(forKey: "useDockerSetup") + // Only restrict to configured peers when using local core, if not, allow DNS discovery + + self.restrictToConfiguredPeers(useLocalCore) + + if useLocalCore { + let raw = UserDefaults.standard.string(forKey: "corePeerAddresses")?.trimmingCharacters(in: .whitespacesAndNewlines) + let peers = (raw?.isEmpty == false ? raw! : "127.0.0.1:20001") + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespaces) } + .filter { !$0.isEmpty } + + self.clearPeers(); + + for peer in peers { + self.addPeer(peer) + } + } + + // Set user agent to include SwiftDashSDK version from the framework bundle + do { + let bundle = Bundle(for: SPVClient.self) + let version = (bundle.infoDictionary?["CFBundleShortVersionString"] as? String) + ?? (bundle.infoDictionary?["CFBundleVersion"] as? String) + ?? "dev" + let ua = "SwiftDashSDK/\(version)" + + self.setUserAgent(ua) + } + } + + deinit { + dash_spv_ffi_config_destroy(config) + } + + public func setMasternodeSyncEnabled(_ enabled: Bool) { + dash_spv_ffi_config_set_masternode_sync_enabled(config, enabled) + } + + public func setMempoolStrategy(_ strategy: FFIMempoolStrategy) { + dash_spv_ffi_config_set_mempool_tracking(config, true) + dash_spv_ffi_config_set_mempool_strategy(config, strategy) + dash_spv_ffi_config_set_fetch_mempool_transactions(config, true) + } + + public func setUserAgent(_ userAgent: String) { + dash_spv_ffi_config_set_user_agent(config, userAgent) + } + + public func setStartHeight(_ height: UInt32) { + dash_spv_ffi_config_set_start_from_height(config, height) + } + + public func setDataDir(_ dataDir: String) { + dash_spv_ffi_config_set_data_dir(config, dataDir) + } + + public func clearPeers() { + dash_spv_ffi_config_clear_peers(config) + } + + // Supports "ip:port" or bare IP for network-default port + public func addPeer(_ peer: String) { + dash_spv_ffi_config_add_peer(config, peer) + } + + public func restrictToConfiguredPeers(_ enabled: Bool) { + dash_spv_ffi_config_set_restrict_to_configured_peers(config, enabled) + } +} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift index f2f2d17904..9a1a0f4fc1 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Services/WalletService.swift @@ -113,21 +113,11 @@ public class WalletService: ObservableObject { LoggingPreferences.configure() - let dataDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("SPV").appendingPathComponent(network.rawValue).path - - // Ensure the data directory exists before initializing the SPV client - if let dataDir = dataDir { - try? FileManager.default.createDirectory(atPath: dataDir, withIntermediateDirectories: true) - } - // Create SPV client first with dummy handlers to obtain the wallet manager, // then destroy and recreate with real handlers that reference self. - let dummyClient = try! SPVClient( - network: network.sdkNetwork, - dataDir: dataDir, - startHeight: 0, - ) - + let dummyConfig = SPVConfig(network.sdkNetwork) + dummyConfig.setDataDir(NSTemporaryDirectory()) + let dummyClient = try! SPVClient(config: dummyConfig) self.spvClient = dummyClient // Create the SDK wallet manager by reusing the SPV client's shared manager @@ -146,9 +136,7 @@ public class WalletService: ObservableObject { ) self.spvClient = try! SPVClient( - network: network.sdkNetwork, - dataDir: dataDir, - startHeight: 0, + config: createSpvConfig(), eventHandlers: handlers, ) @@ -164,13 +152,6 @@ public class WalletService: ObservableObject { private func initializeNewSPVClient() { 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 - - // Ensure the data directory exists before initializing the SPV client - if let dataDir = dataDir { - try? FileManager.default.createDirectory(atPath: dataDir, withIntermediateDirectories: true) - } - // 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() @@ -187,14 +168,10 @@ public class WalletService: ObservableObject { // 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, + config: createSpvConfig(), eventHandlers: handlers, ) - 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 @@ -204,12 +181,23 @@ public class WalletService: ObservableObject { SDKLogger.log("WalletManager wrapper initialized successfully", minimumLevel: .medium) } + private func createSpvConfig() -> SPVConfig { + let clientConfig = SPVConfig(network.sdkNetwork) + + let dataDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("SPV").appendingPathComponent(network.rawValue).path + + clientConfig.setDataDir(dataDir!) + clientConfig.setMempoolStrategy(FFIMempoolStrategy(rawValue: 0)) // FetchAll + clientConfig.setStartHeight(0) // Placeholder + clientConfig.setMasternodeSyncEnabled(masternodesEnabled) + return clientConfig + } + // MARK: - Trusted Mode / Masternode Sync public func setMasternodesEnabled(_ enabled: Bool) { masternodesEnabled = enabled - // Try to apply immediately if the client exists - do { try spvClient.setMasternodeSyncEnabled(enabled) } catch { /* ignore */ } + self.spvClient.updateConfig(createSpvConfig()) } public func disableMasternodeSync() { setMasternodesEnabled(false) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/HDWallet.swift b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/HDWallet.swift index 1290e3f486..abcb8045c8 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/HDWallet.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/HDWallet.swift @@ -14,10 +14,10 @@ public final class HDWallet { // FFI Wallet ID (32 bytes) - links to the rust-dashcore wallet @Attribute(.unique) public var walletId: Data - + // Serialized wallet bytes from FFI - used to restore wallet on app restart @Attribute(.unique) public var serializedWalletBytes: Data - + public init(walletId: Data, serializedWalletBytes: Data, label: String, network: AppNetwork, isWatchOnly: Bool = false, isImported: Bool = false) { self.id = UUID() self.label = label