Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,13 @@ class SPVClient: @unchecked Sendable {
config = configPtr
}

/// Raw FFI client pointer for use as context provider handle.
/// The caller must not free or retain this pointer beyond the SPVClient's lifetime.
var unsafeFFIClientPointer: UnsafeMutableRawPointer? {
guard let client = client else { return nil }
return UnsafeMutableRawPointer(client)
}

deinit {
self.destroy()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import DashSDKFFI
import Foundation

// MARK: - C Callback: Get quorum public key from SPV

/// C-compatible callback that bridges Platform SDK quorum key requests to the SPV client.
///
/// `handle` is the raw `FFIDashSpvClient*` pointer, passed as `core_handle` in
/// `ContextProviderCallbacks`. The SPV FFI function `ffi_dash_spv_get_quorum_public_key`
/// retrieves the BLS public key for the given quorum from the locally synced masternode list.
///
/// - Parameters:
/// - handle: Raw pointer to `FFIDashSpvClient` (cast from `void*`).
/// - quorumType: The quorum type identifier.
/// - quorumHash: Pointer to 32-byte quorum hash.
/// - coreChainLockedHeight: The core chain locked height for the request.
/// - outPubkey: Pointer to a 48-byte output buffer for the BLS public key.
/// - Returns: `CallbackResult` with `success: true` on success or error details on failure.
func spvGetQuorumPublicKey(
handle: UnsafeMutableRawPointer?,
quorumType: UInt32,
quorumHash: UnsafePointer<UInt8>?,
coreChainLockedHeight: UInt32,
outPubkey: UnsafeMutablePointer<UInt8>?
) -> CallbackResult {
guard let handle = handle else {
return CallbackResult(success: false, error_code: -1, error_message: nil)
}
guard let quorumHash = quorumHash, let outPubkey = outPubkey else {
return CallbackResult(success: false, error_code: -2, error_message: nil)
}

let client = handle.assumingMemoryBound(to: FFIDashSpvClient.self)
let ffiResult = ffi_dash_spv_get_quorum_public_key(
client, quorumType, quorumHash, coreChainLockedHeight, outPubkey, 48
)

if ffiResult.error_code == 0 {
return CallbackResult(success: true, error_code: 0, error_message: nil)
} else {
return CallbackResult(
success: false,
error_code: ffiResult.error_code,
error_message: ffiResult.error_message
)
}
}

// MARK: - C Callback: Get platform activation height from SPV

/// C-compatible callback that bridges Platform SDK activation height requests to the SPV client.
///
/// `handle` is the raw `FFIDashSpvClient*` pointer. The SPV FFI function
/// `ffi_dash_spv_get_platform_activation_height` returns the core block height at which
/// Platform was activated, as determined from the locally synced chain.
///
/// - Parameters:
/// - handle: Raw pointer to `FFIDashSpvClient` (cast from `void*`).
/// - outHeight: Pointer to a `UInt32` where the activation height will be written.
/// - Returns: `CallbackResult` with `success: true` on success or error details on failure.
func spvGetPlatformActivationHeight(
handle: UnsafeMutableRawPointer?,
outHeight: UnsafeMutablePointer<UInt32>?
) -> CallbackResult {
guard let handle = handle else {
return CallbackResult(success: false, error_code: -1, error_message: nil)
}
guard let outHeight = outHeight else {
return CallbackResult(success: false, error_code: -2, error_message: nil)
}

let client = handle.assumingMemoryBound(to: FFIDashSpvClient.self)
let ffiResult = ffi_dash_spv_get_platform_activation_height(client, outHeight)

if ffiResult.error_code == 0 {
return CallbackResult(success: true, error_code: 0, error_message: nil)
} else {
return CallbackResult(
success: false,
error_code: ffiResult.error_code,
error_message: ffiResult.error_message
)
}
}

// MARK: - Helper to create ContextProviderCallbacks

/// Creates a `ContextProviderCallbacks` struct configured to use the SPV client
/// for quorum key lookups and platform activation height.
///
/// The returned struct is suitable for passing to `dash_sdk_create_with_callbacks`.
/// The SPV client must remain alive for the entire lifetime of the SDK instance
/// that uses these callbacks.
///
/// - Parameter spvClientHandle: Raw pointer to the `FFIDashSpvClient`, obtained
/// from `SPVClient.unsafeFFIClientPointer`.
/// - Returns: A `ContextProviderCallbacks` ready to pass to `dash_sdk_create_with_callbacks`.
func makeSPVContextProviderCallbacks(spvClientHandle: UnsafeMutableRawPointer) -> ContextProviderCallbacks {
return ContextProviderCallbacks(
core_handle: spvClientHandle,
get_platform_activation_height: spvGetPlatformActivationHeight,
get_quorum_public_key: spvGetQuorumPublicKey
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ public class WalletService: ObservableObject {
private var spvClient: SPVClient
public private(set) var walletManager: CoreWalletManager

/// Raw FFI client pointer for Platform SDK quorum callbacks.
/// The returned pointer is only valid while this WalletService (and its SPV client) is alive.
public var spvClientHandle: UnsafeMutableRawPointer? {
spvClient.unsafeFFIClientPointer
}

public init(modelContainer: ModelContainer, network: AppNetwork) {
self.modelContainer = modelContainer
self.network = network
Expand Down
51 changes: 51 additions & 0 deletions packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,57 @@ public final class SDK: @unchecked Sendable {
self.network = network
}

/// Create a new SDK instance using SPV-synced quorum data for proof verification.
///
/// Instead of fetching quorum keys from a trusted HTTP endpoint, this uses
/// quorum data already synced by the SPV client (masternode list sync).
///
/// - Parameters:
/// - network: The Dash network to connect to.
/// - spvClientHandle: Raw pointer to the SPV client's `FFIDashSpvClient` handle.
/// Obtain this from `SPVClient.unsafeFFIClientPointer`.
/// The SPV client must remain alive for the lifetime of this SDK instance.
public init(network: Network, spvClientHandle: UnsafeMutableRawPointer) throws {
NSLog("SDK.init: Creating SDK with SPV quorum provider, network: \(network)")
var config = DashSDKConfig()
config.network = network
config.dapi_addresses = nil
config.skip_asset_lock_proof_verification = false
config.request_retry_count = 1
config.request_timeout_ms = 8000

var callbacks = makeSPVContextProviderCallbacks(spvClientHandle: spvClientHandle)

let result: DashSDKResult
let forceLocal = UserDefaults.standard.bool(forKey: "useLocalhostPlatform")
if forceLocal {
let localAddresses = Self.platformDAPIAddresses
NSLog("SDK.init: Using local DAPI addresses with SPV quorums: \(localAddresses)")
result = localAddresses.withCString { addressesCStr -> DashSDKResult in
var mutableConfig = config
mutableConfig.dapi_addresses = addressesCStr
return dash_sdk_create_with_callbacks(&mutableConfig, &callbacks)
}
} else {
result = dash_sdk_create_with_callbacks(&config, &callbacks)
}

if result.error != nil {
let error = result.error!.pointee
let errorMessage = error.message != nil ? String(cString: error.message!) : "Unknown error"
defer { dash_sdk_error_free(result.error) }
throw SDKError.internalError("Failed to create SDK with SPV quorums: \(errorMessage)")
}

guard result.data != nil else {
throw SDKError.internalError("No SDK handle returned")
}

handle = result.data?.assumingMemoryBound(to: SDKHandle.self)
self.network = network
NSLog("SDK.init: SDK created with SPV quorum provider")
}

/// Load known contracts into the trusted context provider
/// This avoids network calls for these contracts when they're needed
public func loadKnownContracts(_ contracts: [(id: String, data: Data)]) throws {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,17 @@ class AppState: ObservableObject {
}
}

@Published var useTrustedQuorumFallback: Bool {
didSet {
UserDefaults.standard.set(useTrustedQuorumFallback, forKey: "useTrustedQuorumFallback")
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private let testSigner = TestSigner()
private var dataManager: DataManager?
private var modelContext: ModelContext?
/// Stored SPV client handle for reuse during network switches.
private var spvClientHandle: UnsafeMutableRawPointer?

init() {
// Load saved network preference or use default
Expand All @@ -60,11 +68,16 @@ class AppState: ObservableObject {
let hasCoreKey = UserDefaults.standard.object(forKey: "useLocalhostCore") != nil
self.useLocalPlatform = hasPlatformKey ? UserDefaults.standard.bool(forKey: "useLocalhostPlatform") : legacyLocal
self.useLocalCore = hasCoreKey ? UserDefaults.standard.bool(forKey: "useLocalhostCore") : legacyLocal
self.useTrustedQuorumFallback = UserDefaults.standard.object(forKey: "useTrustedQuorumFallback") != nil
? UserDefaults.standard.bool(forKey: "useTrustedQuorumFallback")
: true // Default: ON (use trusted HTTP quorums as fallback)
}

func initializeSDK(modelContext: ModelContext) {
func initializeSDK(modelContext: ModelContext, spvClientHandle: UnsafeMutableRawPointer? = nil) {
// Save the model context for later use
self.modelContext = modelContext
// Store the SPV handle for reuse during network switches
self.spvClientHandle = spvClientHandle

// Initialize DataManager
self.dataManager = DataManager(modelContext: modelContext, currentNetwork: currentNetwork)
Expand All @@ -73,22 +86,43 @@ class AppState: ObservableObject {
do {
isLoading = true

NSLog("🔵 AppState: Initializing SDK library...")
NSLog("AppState: Initializing SDK library...")
// Initialize the SDK library
SDK.initialize()

// Enable debug logging to see gRPC endpoints
SDK.enableLogging(level: .debug)
NSLog("🔵 AppState: Enabled debug logging for gRPC requests")
NSLog("AppState: Enabled debug logging for gRPC requests")

NSLog("🔵 AppState: Creating SDK instance for network: \(currentNetwork)")
NSLog("AppState: Creating SDK instance for network: \(currentNetwork)")
// Create SDK instance for current network
let sdkNetwork: DashSDKNetwork = currentNetwork.sdkNetwork
NSLog("🔵 AppState: SDK network value: \(sdkNetwork)")
NSLog("AppState: SDK network value: \(sdkNetwork)")

let newSDK: SDK

// Try SPV quorums first if handle is available
if let spvHandle = spvClientHandle {
do {
newSDK = try SDK(network: sdkNetwork, spvClientHandle: spvHandle)
NSLog("AppState: SDK created with SPV quorum provider")
} catch {
if useTrustedQuorumFallback {
NSLog("AppState: SPV quorum provider failed (\(error.localizedDescription)), falling back to trusted")
newSDK = try SDK(network: sdkNetwork)
} else {
throw error
}
}
} else if useTrustedQuorumFallback {
NSLog("AppState: No SPV client available, using trusted quorum provider")
newSDK = try SDK(network: sdkNetwork)
} else {
throw SDKError.invalidState("No SPV client available and trusted fallback disabled")
}

let newSDK = try SDK(network: sdkNetwork)
sdk = newSDK
NSLog("AppState: SDK created successfully with handle: \(newSDK.handle != nil ? "exists" : "nil")")
NSLog("AppState: SDK created successfully with handle: \(newSDK.handle != nil ? "exists" : "nil")")

// Load known contracts into the SDK's trusted provider
await loadKnownContractsIntoSDK(sdk: newSDK, modelContext: modelContext)
Expand All @@ -104,6 +138,11 @@ class AppState: ObservableObject {
}
}

/// Update the stored SPV client handle (e.g., after a network switch recreates the SPV client).
func updateSPVClientHandle(_ handle: UnsafeMutableRawPointer?) {
self.spvClientHandle = handle
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func loadPersistedData() async {
guard let dataManager = dataManager else { return }

Expand Down Expand Up @@ -189,7 +228,28 @@ class AppState: ObservableObject {

// Create new SDK instance for the network
let sdkNetwork: DashSDKNetwork = network.sdkNetwork
let newSDK = try SDK(network: sdkNetwork)

let newSDK: SDK

// Try SPV quorums first if handle is available
if let spvHandle = spvClientHandle {
do {
newSDK = try SDK(network: sdkNetwork, spvClientHandle: spvHandle)
NSLog("AppState.switchNetwork: SDK created with SPV quorum provider")
} catch {
if useTrustedQuorumFallback {
NSLog("AppState.switchNetwork: SPV quorum provider failed (\(error.localizedDescription)), falling back to trusted")
newSDK = try SDK(network: sdkNetwork)
} else {
throw error
}
}
} else if useTrustedQuorumFallback {
newSDK = try SDK(network: sdkNetwork)
} else {
throw SDKError.invalidState("No SPV client available and trusted fallback disabled")
}

sdk = newSDK

// Load known contracts into the SDK's trusted provider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,12 @@ class UnifiedAppState: ObservableObject {
}

func initialize() async {
// Initialize Platform SDK
// Get SPV client handle for Platform SDK quorum verification
let spvHandle = walletService.spvClientHandle

// Initialize Platform SDK with SPV quorums when available
await MainActor.run {
platformState.initializeSDK(modelContext: modelContainer.mainContext)
platformState.initializeSDK(modelContext: modelContainer.mainContext, spvClientHandle: spvHandle)
}

// Wait for Platform SDK to be ready
Expand Down Expand Up @@ -120,9 +123,15 @@ class UnifiedAppState: ObservableObject {

// Handle network switching - called when platformState.currentNetwork changes
func handleNetworkSwitch(to network: AppNetwork) async {
// Switch wallet service to new network (convert to DashNetwork)
// Switch wallet service to new network (which recreates the SPV client)
await walletService.switchNetwork(to: network)

// Update the SPV client handle in platform state (the old handle is now invalid)
let newSpvHandle = walletService.spvClientHandle
await MainActor.run {
platformState.updateSPVClientHandle(newSpvHandle)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

// Reinitialize shielded service for the new network
initializeShieldedService()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ struct OptionsView: View {
}
.help("When enabled, Core (SPV) connects only to configured peers (default 127.0.0.1 with network port). Override via 'corePeerAddresses'.")

Toggle("Fallback to Trusted Quorums", isOn: $appState.useTrustedQuorumFallback)
.help("When enabled, falls back to trusted HTTP quorum provider if SPV quorum data is unavailable. Disable to require SPV-synced quorums for proof verification.")

HStack {
Text("Network Status")
Spacer()
Expand Down
Loading