Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 12 additions & 116 deletions packages/swift-sdk/Sources/SwiftDashSDK/Core/SPV/SPVClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -18,11 +13,11 @@ import Foundation
public class SPVClient: @unchecked Sendable {
private var spvEventHandlers: SPVEventHandlers?

// FFI handles
// FFI handle
private var client: UnsafeMutablePointer<FFIDashSpvClient>?
private var config: UnsafeMutablePointer<FFIClientConfig>?

// 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" {
Expand All @@ -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<FFIClientConfig>? = {
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 {
Expand All @@ -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).
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import Foundation
import DashSDKFFI

public class SPVConfig: @unchecked Sendable {
internal var config: UnsafeMutablePointer<FFIClientConfig>

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: ",")
Comment thread
ZocoLini marked this conversation as resolved.
.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)
Comment thread
ZocoLini marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -146,9 +136,7 @@ public class WalletService: ObservableObject {
)

self.spvClient = try! SPVClient(
network: network.sdkNetwork,
dataDir: dataDir,
startHeight: 0,
config: createSpvConfig(),
eventHandlers: handlers,
)

Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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
Comment thread
ZocoLini marked this conversation as resolved.
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)
Expand Down
Loading
Loading