From 22365d91e4535580a625e90072d722c1524a45c6 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Mon, 25 May 2026 03:34:17 -0500 Subject: [PATCH 1/8] feat(ffi): expose SDK protocol version pinning --- packages/rs-sdk-ffi/README.md | 36 +- packages/rs-sdk-ffi/src/sdk.rs | 610 ++++++++++++++++++ .../swift-sdk/Sources/SwiftDashSDK/SDK.swift | 9 +- 3 files changed, 627 insertions(+), 28 deletions(-) diff --git a/packages/rs-sdk-ffi/README.md b/packages/rs-sdk-ffi/README.md index 16eb502a99b..613621949fc 100644 --- a/packages/rs-sdk-ffi/README.md +++ b/packages/rs-sdk-ffi/README.md @@ -84,8 +84,9 @@ DashSDKConfig config = { .request_timeout_ms = 30000 }; -// Create SDK instance -DashSDKResult result = dash_sdk_create(&config); +// Create SDK instance pinned to Platform protocol version 11. +// Pass 0 to keep the default auto-detect behavior. +DashSDKResult result = dash_sdk_create_with_protocol_version(&config, 11); if (result.error) { // Handle error dash_sdk_error_free(result.error); @@ -106,28 +107,11 @@ dash_sdk_destroy(sdk); // Initialize the SDK dash_sdk_init() -// Create SDK configuration -var config = DashSDKConfig( - network: DashSDKNetwork.testnet, - dapi_addresses: "seed-1.testnet.networks.dash.org".cString(using: .utf8), - request_retry_count: 3, - request_timeout_ms: 30000 -) - -// Create SDK instance -let result = dash_sdk_create(&config) -if let error = result.error { - // Handle error - dash_sdk_error_free(error) - return -} +// Default behavior auto-detects the protocol version. +let sdk = try SDK(network: .testnet) -let sdk = result.data - -// Use the SDK... - -// Clean up -dash_sdk_destroy(sdk) +// Pass 11 to pin Platform protocol version 11 instead. +let pinnedSDK = try SDK(network: .testnet, protocolVersion: 11) ``` ### Python Usage Example @@ -158,8 +142,9 @@ config = DashSDKConfig( request_timeout_ms=30000 ) -# Create SDK instance -result = lib.dash_sdk_create(byref(config)) +# Create SDK instance pinned to protocol version 11. +# Pass 0 to keep the default auto-detect behavior. +result = lib.dash_sdk_create_with_protocol_version(byref(config), 11) # ... handle result and use SDK ``` @@ -170,6 +155,7 @@ result = lib.dash_sdk_create(byref(config)) #### Core Functions - `dash_sdk_init()` - Initialize the FFI library - `dash_sdk_create()` - Create an SDK instance +- `dash_sdk_create_with_protocol_version()` - Create an SDK instance with optional protocol-version pinning - `dash_sdk_destroy()` - Destroy an SDK instance - `dash_sdk_version()` - Get the SDK version diff --git a/packages/rs-sdk-ffi/src/sdk.rs b/packages/rs-sdk-ffi/src/sdk.rs index aa53fa8fcc9..18303c8349e 100644 --- a/packages/rs-sdk-ffi/src/sdk.rs +++ b/packages/rs-sdk-ffi/src/sdk.rs @@ -5,6 +5,7 @@ use tokio::runtime::Runtime; use tracing::{debug, error, info, warn}; use dash_sdk::dpp::serialization::PlatformDeserializableWithPotentialValidationFromVersionedStructure; +use dash_sdk::dpp::version::PlatformVersion; use dash_sdk::sdk::AddressList; use dash_sdk::{Sdk, SdkBuilder}; use std::ffi::CStr; @@ -88,6 +89,23 @@ fn init_or_get_runtime() -> Result, String> { Ok(arc) } +fn resolve_platform_version( + protocol_version: u32, +) -> Result, DashSDKResult> { + if protocol_version == 0 { + return Ok(None); + } + + PlatformVersion::get(protocol_version) + .map(Some) + .map_err(|e| { + DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + format!("Unknown protocol version {}: {}", protocol_version, e), + )) + }) +} + /// Create a new SDK instance /// /// # Safety @@ -167,6 +185,101 @@ pub unsafe extern "C" fn dash_sdk_create(config: *const DashSDKConfig) -> DashSD } } +/// Create a new SDK instance with an explicit protocol version override +/// +/// `protocol_version == 0` preserves the default auto-detect behavior. +/// +/// # Safety +/// - `config` must be a valid pointer to a DashSDKConfig structure for the duration of the call. +/// - The returned handle inside `DashSDKResult` must be destroyed using the SDK destroy function to avoid leaks. +#[no_mangle] +pub unsafe extern "C" fn dash_sdk_create_with_protocol_version( + config: *const DashSDKConfig, + protocol_version: u32, +) -> DashSDKResult { + if config.is_null() { + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + "Config is null".to_string(), + )); + } + + let config = &*config; + + // Parse configuration + let network: Network = config.network.into(); + + let platform_version = match resolve_platform_version(protocol_version) { + Ok(platform_version) => platform_version, + Err(result) => return result, + }; + + // Use shared runtime + let runtime = match init_or_get_runtime() { + Ok(rt) => rt, + Err(e) => { + return DashSDKResult::error(DashSDKError::new(DashSDKErrorCode::InternalError, e)); + } + }; + + // Parse DAPI addresses + let builder = if config.dapi_addresses.is_null() { + // Use mock SDK if no addresses provided + SdkBuilder::new_mock().with_network(network) + } else { + let addresses_str = match unsafe { CStr::from_ptr(config.dapi_addresses) }.to_str() { + Ok(s) => s, + Err(e) => { + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + format!("Invalid DAPI addresses string: {}", e), + )) + } + }; + + if addresses_str.is_empty() { + // Use mock SDK if addresses string is empty + SdkBuilder::new_mock().with_network(network) + } else { + // Parse the address list + let address_list = match AddressList::from_str(addresses_str) { + Ok(list) => list, + Err(e) => { + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + format!("Failed to parse DAPI addresses: {}", e), + )) + } + }; + + SdkBuilder::new(address_list).with_network(network) + } + }; + + let builder = if let Some(platform_version) = platform_version { + builder.with_version(platform_version) + } else { + builder + }; + + // Build SDK + let sdk_result = builder.build().map_err(FFIError::from); + + match sdk_result { + Ok(sdk) => { + // Clone Arc into the wrapper + let wrapper = Box::new(SDKWrapper { + sdk, + runtime, + trusted_provider: None, + }); + let handle = Box::into_raw(wrapper) as *mut SDKHandle; + DashSDKResult::success(handle as *mut std::os::raw::c_void) + } + Err(e) => DashSDKResult::error(e.into()), + } +} + /// Create a new SDK instance with extended configuration including context provider /// /// # Safety @@ -274,6 +387,127 @@ pub unsafe extern "C" fn dash_sdk_create_extended( } } +/// Create a new SDK instance with extended configuration and an explicit protocol version override +/// +/// `protocol_version == 0` preserves the default auto-detect behavior. +/// +/// # Safety +/// - `config` must be a valid pointer to a DashSDKConfigExtended structure for the duration of the call. +/// - Any embedded pointers (context_provider/core_sdk_handle) must be valid when non-null. +/// - The returned handle inside `DashSDKResult` must be destroyed using the SDK destroy function to avoid leaks. +#[no_mangle] +pub unsafe extern "C" fn dash_sdk_create_extended_with_protocol_version( + config: *const DashSDKConfigExtended, + protocol_version: u32, +) -> DashSDKResult { + if config.is_null() { + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + "Config is null".to_string(), + )); + } + + let config = &*config; + let base_config = &config.base_config; + + // Parse configuration + let network: Network = base_config.network.into(); + + let platform_version = match resolve_platform_version(protocol_version) { + Ok(platform_version) => platform_version, + Err(result) => return result, + }; + + // Use shared runtime + let runtime = match init_or_get_runtime() { + Ok(rt) => rt, + Err(e) => { + return DashSDKResult::error(DashSDKError::new(DashSDKErrorCode::InternalError, e)); + } + }; + + // Parse DAPI addresses + let builder = if base_config.dapi_addresses.is_null() { + // Use mock SDK if no addresses provided + SdkBuilder::new_mock().with_network(network) + } else { + let addresses_str = match unsafe { CStr::from_ptr(base_config.dapi_addresses) }.to_str() { + Ok(s) => s, + Err(e) => { + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + format!("Invalid DAPI addresses string: {}", e), + )) + } + }; + + if addresses_str.is_empty() { + // Use mock SDK if addresses string is empty + SdkBuilder::new_mock().with_network(network) + } else { + // Parse the address list + let address_list = match AddressList::from_str(addresses_str) { + Ok(list) => list, + Err(e) => { + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + format!("Failed to parse DAPI addresses: {}", e), + )) + } + }; + + SdkBuilder::new(address_list).with_network(network) + } + }; + + let mut builder = if let Some(platform_version) = platform_version { + builder.with_version(platform_version) + } else { + builder + }; + + // Check if context provider is provided + if !config.context_provider.is_null() { + let provider_wrapper = &*(config.context_provider as *const ContextProviderWrapper); + builder = builder.with_context_provider(provider_wrapper.provider()); + } else if !config.core_sdk_handle.is_null() { + // Use registered global callbacks if available; otherwise return an error + if let Some(callback_provider) = + crate::context_callbacks::CallbackContextProvider::from_global() + { + builder = builder.with_context_provider(callback_provider); + } else { + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InternalError, + "Failed to create context provider. Make sure to call dash_sdk_register_context_callbacks first.".to_string(), + )); + } + } else { + // No context provider specified - try to use global callbacks if available + if let Some(callback_provider) = + crate::context_callbacks::CallbackContextProvider::from_global() + { + builder = builder.with_context_provider(callback_provider); + } + } + + // Build SDK + let sdk_result = builder.build().map_err(FFIError::from); + + match sdk_result { + Ok(sdk) => { + let wrapper = Box::new(SDKWrapper { + sdk, + runtime, + trusted_provider: None, + }); + let handle = Box::into_raw(wrapper) as *mut SDKHandle; + DashSDKResult::success(handle as *mut std::os::raw::c_void) + } + Err(e) => DashSDKResult::error(e.into()), + } +} + /// Create a new SDK instance with trusted setup /// /// This creates an SDK with a trusted context provider that fetches quorum keys and @@ -463,6 +697,211 @@ pub unsafe extern "C" fn dash_sdk_create_trusted(config: *const DashSDKConfig) - } } +/// Create a new SDK instance with trusted setup and an explicit protocol version override +/// +/// This creates an SDK with a trusted context provider that fetches quorum keys and +/// data contracts from trusted endpoints instead of requiring proof verification. +/// +/// `protocol_version == 0` preserves the default auto-detect behavior. +/// +/// # Safety +/// - `config` must be a valid pointer to a DashSDKConfig structure +/// # Safety +/// - `config` must be a valid pointer to a DashSDKConfig structure for the duration of the call. +/// - The returned handle inside `DashSDKResult` must be destroyed using the SDK destroy function to avoid leaks. +#[no_mangle] +pub unsafe extern "C" fn dash_sdk_create_trusted_with_protocol_version( + config: *const DashSDKConfig, + protocol_version: u32, +) -> DashSDKResult { + if config.is_null() { + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + "Config is null".to_string(), + )); + } + + let config = &*config; + + // Parse configuration + let network: Network = config.network.into(); + + let platform_version = match resolve_platform_version(protocol_version) { + Ok(platform_version) => platform_version, + Err(result) => return result, + }; + + // Use shared runtime + let runtime = match init_or_get_runtime() { + Ok(rt) => rt, + Err(e) => { + return DashSDKResult::error(DashSDKError::new(DashSDKErrorCode::InternalError, e)); + } + }; + + info!( + ?network, + "dash_sdk_create_trusted: creating trusted context provider" + ); + + // Create trusted context provider + // For regtest, use the quorum sidecar at localhost:22444 (dashmate Docker default) + let is_local = matches!(network, Network::Regtest); + let trusted_provider = if is_local { + info!("dash_sdk_create_trusted: using local quorum sidecar for regtest"); + match rs_sdk_trusted_context_provider::TrustedHttpContextProvider::new_with_url( + network, + "http://127.0.0.1:22444".to_string(), + std::num::NonZeroUsize::new(100).unwrap(), + ) { + Ok(provider) => { + info!("dash_sdk_create_trusted: local trusted context provider created"); + Arc::new(provider) + } + Err(e) => { + error!(error = %e, "dash_sdk_create_trusted: failed to create local context provider"); + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InternalError, + format!("Failed to create local context provider: {}", e), + )); + } + } + } else { + match rs_sdk_trusted_context_provider::TrustedHttpContextProvider::new( + network, + None, // Use default quorum lookup endpoints + std::num::NonZeroUsize::new(100).unwrap(), // Cache size + ) { + Ok(provider) => { + info!("dash_sdk_create_trusted: trusted context provider created"); + Arc::new(provider) + } + Err(e) => { + error!(error = %e, "dash_sdk_create_trusted: failed to create trusted context provider"); + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InternalError, + format!("Failed to create trusted context provider: {}", e), + )); + } + } + }; + + // Parse DAPI addresses - for trusted setup, we always need real addresses + let builder = if config.dapi_addresses.is_null() { + info!("dash_sdk_create_trusted: no DAPI addresses provided, using defaults for network"); + // Use default addresses for the network + match network { + Network::Testnet => SdkBuilder::new_testnet(), + Network::Mainnet => SdkBuilder::new_mainnet(), + _ => { + error!( + ?network, + "dash_sdk_create_trusted: no DAPI addresses for network" + ); + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + format!("DAPI addresses not available for network: {:?}", network), + )); + } + } + } else { + let addresses_str = match unsafe { CStr::from_ptr(config.dapi_addresses) }.to_str() { + Ok(s) => s, + Err(e) => { + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + format!("Invalid DAPI addresses string: {}", e), + )) + } + }; + + if addresses_str.is_empty() { + error!("dash_sdk_create_trusted: empty DAPI addresses provided"); + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + "DAPI addresses cannot be empty for trusted setup".to_string(), + )); + } else { + info!( + addresses = addresses_str, + "dash_sdk_create_trusted: using provided DAPI addresses" + ); + // Parse the address list + let address_list = match AddressList::from_str(addresses_str) { + Ok(list) => { + info!("dash_sdk_create_trusted: successfully parsed addresses"); + list + } + Err(e) => { + error!(error = %e, "dash_sdk_create_trusted: failed to parse addresses"); + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + format!("Failed to parse DAPI addresses: {}", e), + )); + } + }; + + SdkBuilder::new(address_list).with_network(network) + } + }; + + let builder = if let Some(platform_version) = platform_version { + builder.with_version(platform_version) + } else { + builder + }; + + // Clone trusted provider for prefetching quorums + let provider_for_prefetch = Arc::clone(&trusted_provider); + let provider_for_wrapper = Arc::clone(&trusted_provider); + + // Add trusted context provider + info!("dash_sdk_create_trusted: adding trusted context provider to builder"); + let builder = builder.with_context_provider(Arc::clone(&trusted_provider)); + + // Build SDK + let sdk_result = builder.build().map_err(FFIError::from); + + match sdk_result { + Ok(sdk) => { + // Prefetch quorums for trusted setup + info!("dash_sdk_create_trusted: SDK built, prefetching quorums..."); + + let runtime_clone = runtime.handle().clone(); + runtime_clone.spawn(async move { + // First, try a simple HTTP test + debug!("dash_sdk_create_trusted: testing basic HTTP connectivity"); + match reqwest::get("https://www.google.com").await { + Ok(_) => debug!("dash_sdk_create_trusted: basic HTTP test successful (Google)"), + Err(e) => warn!(error = %e, "dash_sdk_create_trusted: basic HTTP test failed"), + } + + // Try the quorums endpoint directly + debug!("dash_sdk_create_trusted: testing quorums endpoint directly"); + match reqwest::get("https://quorums.testnet.networks.dash.org/quorums").await { + Ok(resp) => debug!(status = %resp.status(), "dash_sdk_create_trusted: direct quorums endpoint test successful"), + Err(e) => warn!(error = %e, "dash_sdk_create_trusted: direct quorums endpoint test failed"), + } + + // Now try through the provider + match provider_for_prefetch.update_quorum_caches().await { + Ok(_) => info!("dash_sdk_create_trusted: successfully prefetched quorums"), + Err(e) => warn!(error = %e, "dash_sdk_create_trusted: failed to prefetch quorums; continuing"), + } + }); + + let wrapper = Box::new(SDKWrapper { + sdk, + runtime, + trusted_provider: Some(provider_for_wrapper), + }); + let handle = Box::into_raw(wrapper) as *mut SDKHandle; + DashSDKResult::success(handle as *mut std::os::raw::c_void) + } + Err(e) => DashSDKResult::error(e.into()), + } +} + /// Destroy an SDK instance /// # Safety /// - `handle` must be a valid pointer previously returned by this SDK and not yet destroyed. @@ -587,6 +1026,177 @@ pub unsafe extern "C" fn dash_sdk_create_with_callbacks( result } +/// Create a new SDK instance with explicit context callbacks and an explicit protocol version override +/// +/// This is an alternative to registering global callbacks. The callbacks are used only for this SDK instance. +/// +/// `protocol_version == 0` preserves the default auto-detect behavior. +/// +/// # Safety +/// - `config` must be a valid pointer to a DashSDKConfig structure +/// - `callbacks` must contain valid function pointers that remain valid for the lifetime of the SDK +#[no_mangle] +pub unsafe extern "C" fn dash_sdk_create_with_callbacks_and_protocol_version( + config: *const DashSDKConfig, + callbacks: *const crate::context_callbacks::ContextProviderCallbacks, + protocol_version: u32, +) -> DashSDKResult { + if config.is_null() { + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + "Config is null".to_string(), + )); + } + + if callbacks.is_null() { + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + "Callbacks is null".to_string(), + )); + } + + // Create extended config with callback-based context provider + let callbacks = &*callbacks; + let context_provider = crate::context_callbacks::CallbackContextProvider::new( + crate::context_callbacks::ContextProviderCallbacks { + core_handle: callbacks.core_handle, + get_platform_activation_height: callbacks.get_platform_activation_height, + get_quorum_public_key: callbacks.get_quorum_public_key, + }, + ); + + let wrapper = Box::new(ContextProviderWrapper::new(context_provider)); + let context_provider_handle = Box::into_raw(wrapper) as *mut ContextProviderHandle; + + // Read fields from the caller's config individually rather than copying + // the struct (which would duplicate the raw `dapi_addresses` pointer). + // The pointer is only borrowed for the duration of this call; the + // downstream `dash_sdk_create_extended` reads and copies the string data + // immediately before returning. + let config_ref = &*config; + let extended_config = DashSDKConfigExtended { + base_config: DashSDKConfig { + network: config_ref.network, + dapi_addresses: config_ref.dapi_addresses, + skip_asset_lock_proof_verification: config_ref.skip_asset_lock_proof_verification, + request_retry_count: config_ref.request_retry_count, + request_timeout_ms: config_ref.request_timeout_ms, + }, + context_provider: context_provider_handle, + core_sdk_handle: std::ptr::null_mut(), + }; + + // Use the extended creation function + let result = dash_sdk_create_extended_with_protocol_version(&extended_config, protocol_version); + + // Reclaim the context provider wrapper - the SDK has already cloned what it needs + // via `provider_wrapper.provider()` inside `dash_sdk_create_extended`. + let _ = Box::from_raw(context_provider_handle as *mut ContextProviderWrapper); + + result +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + dash_sdk_destroy, dash_sdk_error_free, dash_sdk_get_inner_sdk_ptr, DashSDKErrorCode, + FFINetwork, + }; + + unsafe fn read_error_message(error: *mut crate::DashSDKError) -> String { + std::ffi::CStr::from_ptr((*error).message) + .to_string_lossy() + .into_owned() + } + + #[test] + fn dash_sdk_create_with_protocol_version_pins_v11() { + let config = DashSDKConfig { + network: FFINetwork::Testnet, + dapi_addresses: std::ptr::null(), + skip_asset_lock_proof_verification: false, + request_retry_count: 3, + request_timeout_ms: 30_000, + }; + + let result = unsafe { dash_sdk_create_with_protocol_version(&config, 11) }; + assert!(result.error.is_null(), "expected success"); + + let handle = result.data as *mut SDKHandle; + let inner_sdk_ptr = unsafe { dash_sdk_get_inner_sdk_ptr(handle) }; + assert!(!inner_sdk_ptr.is_null(), "expected inner sdk pointer"); + + let sdk = unsafe { &*(inner_sdk_ptr as *const dash_sdk::Sdk) }; + assert_eq!(sdk.protocol_version_number(), 11); + assert_eq!(sdk.version().protocol_version, 11); + + unsafe { + dash_sdk_destroy(handle); + } + } + + #[test] + fn dash_sdk_create_with_protocol_version_rejects_invalid_protocol_version() { + let config = DashSDKConfig { + network: FFINetwork::Testnet, + dapi_addresses: std::ptr::null(), + skip_asset_lock_proof_verification: false, + request_retry_count: 3, + request_timeout_ms: 30_000, + }; + + let result = unsafe { dash_sdk_create_with_protocol_version(&config, u32::MAX) }; + assert!(!result.error.is_null(), "expected invalid parameter error"); + + let error = unsafe { &*result.error }; + assert_eq!(error.code, DashSDKErrorCode::InvalidParameter); + let message = unsafe { read_error_message(result.error) }; + assert!(message.contains("Unknown protocol version")); + + unsafe { + dash_sdk_error_free(result.error); + } + } + + #[test] + fn resolve_platform_version_can_pin_v11_for_trusted_builder_flow() { + let platform_version = match resolve_platform_version(11) { + Ok(Some(platform_version)) => platform_version, + Ok(None) => panic!("expected a pinned platform version"), + Err(_) => panic!("expected protocol version 11"), + }; + let sdk = SdkBuilder::new_mock() + .with_version(platform_version) + .build() + .expect("expected mock SDK build"); + + assert_eq!(sdk.protocol_version_number(), 11); + assert_eq!(sdk.version().protocol_version, 11); + } + + #[test] + fn dash_sdk_create_trusted_with_protocol_version_rejects_invalid_protocol_version() { + let config = DashSDKConfig { + network: FFINetwork::Testnet, + dapi_addresses: std::ptr::null(), + skip_asset_lock_proof_verification: false, + request_retry_count: 3, + request_timeout_ms: 30_000, + }; + + let result = unsafe { dash_sdk_create_trusted_with_protocol_version(&config, u32::MAX) }; + assert!(!result.error.is_null(), "expected invalid parameter error"); + + let error = unsafe { &*result.error }; + assert_eq!(error.code, DashSDKErrorCode::InvalidParameter); + + unsafe { + dash_sdk_error_free(result.error); + } + } +} + /// Get the current network the SDK is connected to /// /// # Safety diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift b/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift index 39147c56ed1..fbf2cdf79ef 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift @@ -94,7 +94,10 @@ public final class SDK: @unchecked Sendable { /// This uses a trusted context provider that fetches quorum keys and /// data contracts from trusted HTTP endpoints instead of requiring proof verification. /// This is suitable for mobile applications where proof verification would be resource-intensive. - public init(network: Network) throws { + /// + /// `protocolVersion == 0` keeps the default auto-detect behavior. + /// `protocolVersion == 11` pins Platform protocol version 11. + public init(network: Network, protocolVersion: UInt32 = 0) throws { var config = DashSDKConfig() config.network = network.ffiValue config.dapi_addresses = nil @@ -121,10 +124,10 @@ public final class SDK: @unchecked Sendable { result = localAddresses.withCString { addressesCStr -> DashSDKResult in var mutableConfig = config mutableConfig.dapi_addresses = addressesCStr - return dash_sdk_create_trusted(&mutableConfig) + return dash_sdk_create_trusted_with_protocol_version(&mutableConfig, protocolVersion) } } else { - result = dash_sdk_create_trusted(&config) + result = dash_sdk_create_trusted_with_protocol_version(&config, protocolVersion) } // Check for errors From ef353c142df3ef770d71eb79dfe104df9efac571 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Mon, 25 May 2026 09:23:09 -0500 Subject: [PATCH 2/8] fix(rs-sdk-ffi): address SDK protocol pinning review --- packages/rs-sdk-ffi/README.md | 3 + packages/rs-sdk-ffi/src/sdk.rs | 905 ++++++++++----------------------- 2 files changed, 276 insertions(+), 632 deletions(-) diff --git a/packages/rs-sdk-ffi/README.md b/packages/rs-sdk-ffi/README.md index 613621949fc..e6971b44f12 100644 --- a/packages/rs-sdk-ffi/README.md +++ b/packages/rs-sdk-ffi/README.md @@ -80,6 +80,7 @@ dash_sdk_init(); DashSDKConfig config = { .network = DASH_SDK_NETWORK_TESTNET, .dapi_addresses = "seed-1.testnet.networks.dash.org", + .skip_asset_lock_proof_verification = false, .request_retry_count = 3, .request_timeout_ms = 30000 }; @@ -131,6 +132,7 @@ class DashSDKConfig(Structure): _fields_ = [ ("network", c_int), ("dapi_addresses", c_char_p), + ("skip_asset_lock_proof_verification", c_bool), ("request_retry_count", c_uint32), ("request_timeout_ms", c_uint64) ] @@ -138,6 +140,7 @@ class DashSDKConfig(Structure): config = DashSDKConfig( network=1, # Testnet dapi_addresses=b"seed-1.testnet.networks.dash.org", + skip_asset_lock_proof_verification=False, request_retry_count=3, request_timeout_ms=30000 ) diff --git a/packages/rs-sdk-ffi/src/sdk.rs b/packages/rs-sdk-ffi/src/sdk.rs index 18303c8349e..73f5c0da1b5 100644 --- a/packages/rs-sdk-ffi/src/sdk.rs +++ b/packages/rs-sdk-ffi/src/sdk.rs @@ -26,6 +26,8 @@ pub struct DashSDKConfigExtended { pub core_sdk_handle: *mut CoreSDKHandle, } +type TrustedProvider = Arc; + /// Internal SDK wrapper pub(crate) struct SDKWrapper { pub sdk: Sdk, @@ -106,319 +108,124 @@ fn resolve_platform_version( }) } -/// Create a new SDK instance -/// -/// # Safety -/// - `config` must be a valid pointer to a DashSDKConfig structure for the duration of the call. -/// - The returned handle inside `DashSDKResult` must be destroyed using the SDK destroy function to avoid leaks. -#[no_mangle] -pub unsafe extern "C" fn dash_sdk_create(config: *const DashSDKConfig) -> DashSDKResult { - if config.is_null() { - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - "Config is null".to_string(), - )); +fn parse_dapi_addresses(config: &DashSDKConfig) -> Result, DashSDKResult> { + if config.dapi_addresses.is_null() { + return Ok(None); } - let config = &*config; - - // Parse configuration - let network: Network = config.network.into(); - - // Use shared runtime - let runtime = match init_or_get_runtime() { - Ok(rt) => rt, - Err(e) => { - return DashSDKResult::error(DashSDKError::new(DashSDKErrorCode::InternalError, e)); - } - }; - - // Parse DAPI addresses - let builder = if config.dapi_addresses.is_null() { - // Use mock SDK if no addresses provided - SdkBuilder::new_mock().with_network(network) - } else { - let addresses_str = match unsafe { CStr::from_ptr(config.dapi_addresses) }.to_str() { - Ok(s) => s, - Err(e) => { - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - format!("Invalid DAPI addresses string: {}", e), - )) - } - }; - - if addresses_str.is_empty() { - // Use mock SDK if addresses string is empty - SdkBuilder::new_mock().with_network(network) - } else { - // Parse the address list - let address_list = match AddressList::from_str(addresses_str) { - Ok(list) => list, - Err(e) => { - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - format!("Failed to parse DAPI addresses: {}", e), - )) - } - }; - - SdkBuilder::new(address_list).with_network(network) - } - }; - - // Build SDK - let sdk_result = builder.build().map_err(FFIError::from); + let addresses_str = unsafe { CStr::from_ptr(config.dapi_addresses) } + .to_str() + .map_err(|e| { + DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + format!("Invalid DAPI addresses string: {}", e), + )) + })?; - match sdk_result { - Ok(sdk) => { - // Clone Arc into the wrapper - let wrapper = Box::new(SDKWrapper { - sdk, - runtime, - trusted_provider: None, - }); - let handle = Box::into_raw(wrapper) as *mut SDKHandle; - DashSDKResult::success(handle as *mut std::os::raw::c_void) - } - Err(e) => DashSDKResult::error(e.into()), + if addresses_str.is_empty() { + return Ok(None); } -} -/// Create a new SDK instance with an explicit protocol version override -/// -/// `protocol_version == 0` preserves the default auto-detect behavior. -/// -/// # Safety -/// - `config` must be a valid pointer to a DashSDKConfig structure for the duration of the call. -/// - The returned handle inside `DashSDKResult` must be destroyed using the SDK destroy function to avoid leaks. -#[no_mangle] -pub unsafe extern "C" fn dash_sdk_create_with_protocol_version( - config: *const DashSDKConfig, - protocol_version: u32, -) -> DashSDKResult { - if config.is_null() { - return DashSDKResult::error(DashSDKError::new( + AddressList::from_str(addresses_str).map(Some).map_err(|e| { + DashSDKResult::error(DashSDKError::new( DashSDKErrorCode::InvalidParameter, - "Config is null".to_string(), - )); - } - - let config = &*config; + format!("Failed to parse DAPI addresses: {}", e), + )) + }) +} - // Parse configuration +fn build_sdk_builder(config: &DashSDKConfig) -> Result { let network: Network = config.network.into(); - - let platform_version = match resolve_platform_version(protocol_version) { - Ok(platform_version) => platform_version, - Err(result) => return result, - }; - - // Use shared runtime - let runtime = match init_or_get_runtime() { - Ok(rt) => rt, - Err(e) => { - return DashSDKResult::error(DashSDKError::new(DashSDKErrorCode::InternalError, e)); - } + let builder = match parse_dapi_addresses(config)? { + Some(address_list) => SdkBuilder::new(address_list), + None => SdkBuilder::new_mock(), }; - // Parse DAPI addresses - let builder = if config.dapi_addresses.is_null() { - // Use mock SDK if no addresses provided - SdkBuilder::new_mock().with_network(network) - } else { - let addresses_str = match unsafe { CStr::from_ptr(config.dapi_addresses) }.to_str() { - Ok(s) => s, - Err(e) => { - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - format!("Invalid DAPI addresses string: {}", e), - )) - } - }; - - if addresses_str.is_empty() { - // Use mock SDK if addresses string is empty - SdkBuilder::new_mock().with_network(network) - } else { - // Parse the address list - let address_list = match AddressList::from_str(addresses_str) { - Ok(list) => list, - Err(e) => { - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - format!("Failed to parse DAPI addresses: {}", e), - )) - } - }; - - SdkBuilder::new(address_list).with_network(network) - } - }; + Ok(builder.with_network(network)) +} - let builder = if let Some(platform_version) = platform_version { +fn apply_platform_version( + builder: SdkBuilder, + platform_version: Option<&'static PlatformVersion>, +) -> SdkBuilder { + if let Some(platform_version) = platform_version { builder.with_version(platform_version) } else { builder - }; - - // Build SDK - let sdk_result = builder.build().map_err(FFIError::from); - - match sdk_result { - Ok(sdk) => { - // Clone Arc into the wrapper - let wrapper = Box::new(SDKWrapper { - sdk, - runtime, - trusted_provider: None, - }); - let handle = Box::into_raw(wrapper) as *mut SDKHandle; - DashSDKResult::success(handle as *mut std::os::raw::c_void) - } - Err(e) => DashSDKResult::error(e.into()), } } -/// Create a new SDK instance with extended configuration including context provider -/// -/// # Safety -/// - `config` must be a valid pointer to a DashSDKConfigExtended structure for the duration of the call. -/// - Any embedded pointers (context_provider/core_sdk_handle) must be valid when non-null. -/// - The returned handle inside `DashSDKResult` must be destroyed using the SDK destroy function to avoid leaks. -#[no_mangle] -pub unsafe extern "C" fn dash_sdk_create_extended( - config: *const DashSDKConfigExtended, -) -> DashSDKResult { - if config.is_null() { - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - "Config is null".to_string(), - )); - } - - let config = &*config; - let base_config = &config.base_config; - - // Parse configuration - let network: Network = base_config.network.into(); - - // Use shared runtime - let runtime = match init_or_get_runtime() { - Ok(rt) => rt, - Err(e) => { - return DashSDKResult::error(DashSDKError::new(DashSDKErrorCode::InternalError, e)); - } - }; - - // Parse DAPI addresses - let mut builder = if base_config.dapi_addresses.is_null() { - // Use mock SDK if no addresses provided - SdkBuilder::new_mock().with_network(network) - } else { - let addresses_str = match unsafe { CStr::from_ptr(base_config.dapi_addresses) }.to_str() { - Ok(s) => s, - Err(e) => { - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - format!("Invalid DAPI addresses string: {}", e), - )) - } - }; - - if addresses_str.is_empty() { - // Use mock SDK if addresses string is empty - SdkBuilder::new_mock().with_network(network) - } else { - // Parse the address list - let address_list = match AddressList::from_str(addresses_str) { - Ok(list) => list, - Err(e) => { - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - format!("Failed to parse DAPI addresses: {}", e), - )) - } - }; - - SdkBuilder::new(address_list).with_network(network) - } - }; - - // Check if context provider is provided +fn apply_context_provider( + mut builder: SdkBuilder, + config: &DashSDKConfigExtended, +) -> Result { if !config.context_provider.is_null() { - let provider_wrapper = &*(config.context_provider as *const ContextProviderWrapper); + let provider_wrapper = + unsafe { &*(config.context_provider as *const ContextProviderWrapper) }; builder = builder.with_context_provider(provider_wrapper.provider()); } else if !config.core_sdk_handle.is_null() { - // Use registered global callbacks if available; otherwise return an error if let Some(callback_provider) = crate::context_callbacks::CallbackContextProvider::from_global() { builder = builder.with_context_provider(callback_provider); } else { - return DashSDKResult::error(DashSDKError::new( + return Err(DashSDKResult::error(DashSDKError::new( DashSDKErrorCode::InternalError, "Failed to create context provider. Make sure to call dash_sdk_register_context_callbacks first.".to_string(), - )); - } - } else { - // No context provider specified - try to use global callbacks if available - if let Some(callback_provider) = - crate::context_callbacks::CallbackContextProvider::from_global() - { - builder = builder.with_context_provider(callback_provider); + ))); } + } else if let Some(callback_provider) = + crate::context_callbacks::CallbackContextProvider::from_global() + { + builder = builder.with_context_provider(callback_provider); } - // Build SDK - let sdk_result = builder.build().map_err(FFIError::from); - - match sdk_result { - Ok(sdk) => { - let wrapper = Box::new(SDKWrapper { - sdk, - runtime, - trusted_provider: None, - }); - let handle = Box::into_raw(wrapper) as *mut SDKHandle; - DashSDKResult::success(handle as *mut std::os::raw::c_void) - } - Err(e) => DashSDKResult::error(e.into()), - } + Ok(builder) } -/// Create a new SDK instance with extended configuration and an explicit protocol version override -/// -/// `protocol_version == 0` preserves the default auto-detect behavior. -/// -/// # Safety -/// - `config` must be a valid pointer to a DashSDKConfigExtended structure for the duration of the call. -/// - Any embedded pointers (context_provider/core_sdk_handle) must be valid when non-null. -/// - The returned handle inside `DashSDKResult` must be destroyed using the SDK destroy function to avoid leaks. -#[no_mangle] -pub unsafe extern "C" fn dash_sdk_create_extended_with_protocol_version( - config: *const DashSDKConfigExtended, - protocol_version: u32, +fn make_sdk_result( + sdk: Sdk, + runtime: Arc, + trusted_provider: Option, ) -> DashSDKResult { - if config.is_null() { - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - "Config is null".to_string(), - )); - } - - let config = &*config; - let base_config = &config.base_config; + let wrapper = Box::new(SDKWrapper { + sdk, + runtime, + trusted_provider, + }); + let handle = Box::into_raw(wrapper) as *mut SDKHandle; + DashSDKResult::success(handle as *mut std::os::raw::c_void) +} - // Parse configuration - let network: Network = base_config.network.into(); +fn create_sdk_from_config( + config: &DashSDKConfig, + platform_version: Option<&'static PlatformVersion>, +) -> DashSDKResult { + let runtime = match init_or_get_runtime() { + Ok(rt) => rt, + Err(e) => { + return DashSDKResult::error(DashSDKError::new(DashSDKErrorCode::InternalError, e)); + } + }; - let platform_version = match resolve_platform_version(protocol_version) { - Ok(platform_version) => platform_version, + let builder = match build_sdk_builder(config) { + Ok(builder) => builder, Err(result) => return result, }; - // Use shared runtime + match apply_platform_version(builder, platform_version) + .build() + .map_err(FFIError::from) + { + Ok(sdk) => make_sdk_result(sdk, runtime, None), + Err(e) => DashSDKResult::error(e.into()), + } +} + +fn create_extended_sdk_from_config( + config: &DashSDKConfigExtended, + platform_version: Option<&'static PlatformVersion>, +) -> DashSDKResult { let runtime = match init_or_get_runtime() { Ok(rt) => rt, Err(e) => { @@ -426,127 +233,33 @@ pub unsafe extern "C" fn dash_sdk_create_extended_with_protocol_version( } }; - // Parse DAPI addresses - let builder = if base_config.dapi_addresses.is_null() { - // Use mock SDK if no addresses provided - SdkBuilder::new_mock().with_network(network) - } else { - let addresses_str = match unsafe { CStr::from_ptr(base_config.dapi_addresses) }.to_str() { - Ok(s) => s, - Err(e) => { - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - format!("Invalid DAPI addresses string: {}", e), - )) - } - }; - - if addresses_str.is_empty() { - // Use mock SDK if addresses string is empty - SdkBuilder::new_mock().with_network(network) - } else { - // Parse the address list - let address_list = match AddressList::from_str(addresses_str) { - Ok(list) => list, - Err(e) => { - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - format!("Failed to parse DAPI addresses: {}", e), - )) - } - }; - - SdkBuilder::new(address_list).with_network(network) - } + let builder = match build_sdk_builder(&config.base_config) { + Ok(builder) => builder, + Err(result) => return result, }; - let mut builder = if let Some(platform_version) = platform_version { - builder.with_version(platform_version) - } else { - builder + let builder = apply_platform_version(builder, platform_version); + let builder = match apply_context_provider(builder, config) { + Ok(builder) => builder, + Err(result) => return result, }; - // Check if context provider is provided - if !config.context_provider.is_null() { - let provider_wrapper = &*(config.context_provider as *const ContextProviderWrapper); - builder = builder.with_context_provider(provider_wrapper.provider()); - } else if !config.core_sdk_handle.is_null() { - // Use registered global callbacks if available; otherwise return an error - if let Some(callback_provider) = - crate::context_callbacks::CallbackContextProvider::from_global() - { - builder = builder.with_context_provider(callback_provider); - } else { - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InternalError, - "Failed to create context provider. Make sure to call dash_sdk_register_context_callbacks first.".to_string(), - )); - } - } else { - // No context provider specified - try to use global callbacks if available - if let Some(callback_provider) = - crate::context_callbacks::CallbackContextProvider::from_global() - { - builder = builder.with_context_provider(callback_provider); - } - } - - // Build SDK - let sdk_result = builder.build().map_err(FFIError::from); - - match sdk_result { - Ok(sdk) => { - let wrapper = Box::new(SDKWrapper { - sdk, - runtime, - trusted_provider: None, - }); - let handle = Box::into_raw(wrapper) as *mut SDKHandle; - DashSDKResult::success(handle as *mut std::os::raw::c_void) - } + match builder.build().map_err(FFIError::from) { + Ok(sdk) => make_sdk_result(sdk, runtime, None), Err(e) => DashSDKResult::error(e.into()), } } -/// Create a new SDK instance with trusted setup -/// -/// This creates an SDK with a trusted context provider that fetches quorum keys and -/// data contracts from trusted endpoints instead of requiring proof verification. -/// -/// # Safety -/// - `config` must be a valid pointer to a DashSDKConfig structure -/// # Safety -/// - `config` must be a valid pointer to a DashSDKConfig structure for the duration of the call. -/// - The returned handle inside `DashSDKResult` must be destroyed using the SDK destroy function to avoid leaks. -#[no_mangle] -pub unsafe extern "C" fn dash_sdk_create_trusted(config: *const DashSDKConfig) -> DashSDKResult { - if config.is_null() { - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - "Config is null".to_string(), - )); - } - - let config = &*config; - - // Parse configuration +fn build_trusted_sdk_builder( + config: &DashSDKConfig, +) -> Result<(SdkBuilder, TrustedProvider), DashSDKResult> { let network: Network = config.network.into(); - // Use shared runtime - let runtime = match init_or_get_runtime() { - Ok(rt) => rt, - Err(e) => { - return DashSDKResult::error(DashSDKError::new(DashSDKErrorCode::InternalError, e)); - } - }; - info!( ?network, "dash_sdk_create_trusted: creating trusted context provider" ); - // Create trusted context provider - // For regtest, use the quorum sidecar at localhost:22444 (dashmate Docker default) let is_local = matches!(network, Network::Regtest); let trusted_provider = if is_local { info!("dash_sdk_create_trusted: using local quorum sidecar for regtest"); @@ -561,17 +274,17 @@ pub unsafe extern "C" fn dash_sdk_create_trusted(config: *const DashSDKConfig) - } Err(e) => { error!(error = %e, "dash_sdk_create_trusted: failed to create local context provider"); - return DashSDKResult::error(DashSDKError::new( + return Err(DashSDKResult::error(DashSDKError::new( DashSDKErrorCode::InternalError, format!("Failed to create local context provider: {}", e), - )); + ))); } } } else { match rs_sdk_trusted_context_provider::TrustedHttpContextProvider::new( network, - None, // Use default quorum lookup endpoints - std::num::NonZeroUsize::new(100).unwrap(), // Cache size + None, + std::num::NonZeroUsize::new(100).unwrap(), ) { Ok(provider) => { info!("dash_sdk_create_trusted: trusted context provider created"); @@ -579,18 +292,16 @@ pub unsafe extern "C" fn dash_sdk_create_trusted(config: *const DashSDKConfig) - } Err(e) => { error!(error = %e, "dash_sdk_create_trusted: failed to create trusted context provider"); - return DashSDKResult::error(DashSDKError::new( + return Err(DashSDKResult::error(DashSDKError::new( DashSDKErrorCode::InternalError, format!("Failed to create trusted context provider: {}", e), - )); + ))); } } }; - // Parse DAPI addresses - for trusted setup, we always need real addresses let builder = if config.dapi_addresses.is_null() { info!("dash_sdk_create_trusted: no DAPI addresses provided, using defaults for network"); - // Use default addresses for the network match network { Network::Testnet => SdkBuilder::new_testnet(), Network::Mainnet => SdkBuilder::new_mainnet(), @@ -599,118 +310,122 @@ pub unsafe extern "C" fn dash_sdk_create_trusted(config: *const DashSDKConfig) - ?network, "dash_sdk_create_trusted: no DAPI addresses for network" ); - return DashSDKResult::error(DashSDKError::new( + return Err(DashSDKResult::error(DashSDKError::new( DashSDKErrorCode::InvalidParameter, format!("DAPI addresses not available for network: {:?}", network), - )); + ))); } } } else { - let addresses_str = match unsafe { CStr::from_ptr(config.dapi_addresses) }.to_str() { - Ok(s) => s, - Err(e) => { - return DashSDKResult::error(DashSDKError::new( + let addresses_str = unsafe { CStr::from_ptr(config.dapi_addresses) } + .to_str() + .map_err(|e| { + DashSDKResult::error(DashSDKError::new( DashSDKErrorCode::InvalidParameter, format!("Invalid DAPI addresses string: {}", e), )) - } - }; + })?; if addresses_str.is_empty() { error!("dash_sdk_create_trusted: empty DAPI addresses provided"); - return DashSDKResult::error(DashSDKError::new( + return Err(DashSDKResult::error(DashSDKError::new( DashSDKErrorCode::InvalidParameter, "DAPI addresses cannot be empty for trusted setup".to_string(), - )); - } else { - info!( - addresses = addresses_str, - "dash_sdk_create_trusted: using provided DAPI addresses" - ); - // Parse the address list - let address_list = match AddressList::from_str(addresses_str) { - Ok(list) => { - info!("dash_sdk_create_trusted: successfully parsed addresses"); - list - } - Err(e) => { - error!(error = %e, "dash_sdk_create_trusted: failed to parse addresses"); - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - format!("Failed to parse DAPI addresses: {}", e), - )); - } - }; - - SdkBuilder::new(address_list).with_network(network) + ))); } - }; - // Clone trusted provider for prefetching quorums - let provider_for_prefetch = Arc::clone(&trusted_provider); - let provider_for_wrapper = Arc::clone(&trusted_provider); + info!( + addresses = addresses_str, + "dash_sdk_create_trusted: using provided DAPI addresses" + ); + let address_list = AddressList::from_str(addresses_str).map_err(|e| { + error!(error = %e, "dash_sdk_create_trusted: failed to parse addresses"); + DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + format!("Failed to parse DAPI addresses: {}", e), + )) + })?; + info!("dash_sdk_create_trusted: successfully parsed addresses"); + + SdkBuilder::new(address_list).with_network(network) + }; - // Add trusted context provider info!("dash_sdk_create_trusted: adding trusted context provider to builder"); - let builder = builder.with_context_provider(Arc::clone(&trusted_provider)); + Ok(( + builder.with_context_provider(Arc::clone(&trusted_provider)), + trusted_provider, + )) +} - // Build SDK - let sdk_result = builder.build().map_err(FFIError::from); +fn spawn_trusted_quorum_prefetch(runtime: &Arc, trusted_provider: TrustedProvider) { + info!("dash_sdk_create_trusted: SDK built, prefetching quorums..."); - match sdk_result { + runtime.handle().spawn(async move { + debug!("dash_sdk_create_trusted: prefetching quorum caches"); + match trusted_provider.update_quorum_caches().await { + Ok(_) => info!("dash_sdk_create_trusted: successfully prefetched quorums"), + Err(e) => { + warn!(error = %e, "dash_sdk_create_trusted: failed to prefetch quorums; continuing") + } + } + }); +} + +fn create_trusted_sdk_from_config( + config: &DashSDKConfig, + platform_version: Option<&'static PlatformVersion>, +) -> DashSDKResult { + let runtime = match init_or_get_runtime() { + Ok(rt) => rt, + Err(e) => { + return DashSDKResult::error(DashSDKError::new(DashSDKErrorCode::InternalError, e)); + } + }; + + let (builder, trusted_provider) = match build_trusted_sdk_builder(config) { + Ok(parts) => parts, + Err(result) => return result, + }; + + let provider_for_wrapper = Arc::clone(&trusted_provider); + match apply_platform_version(builder, platform_version) + .build() + .map_err(FFIError::from) + { Ok(sdk) => { - // Prefetch quorums for trusted setup - info!("dash_sdk_create_trusted: SDK built, prefetching quorums..."); - - let runtime_clone = runtime.handle().clone(); - runtime_clone.spawn(async move { - // First, try a simple HTTP test - debug!("dash_sdk_create_trusted: testing basic HTTP connectivity"); - match reqwest::get("https://www.google.com").await { - Ok(_) => debug!("dash_sdk_create_trusted: basic HTTP test successful (Google)"), - Err(e) => warn!(error = %e, "dash_sdk_create_trusted: basic HTTP test failed"), - } - - // Try the quorums endpoint directly - debug!("dash_sdk_create_trusted: testing quorums endpoint directly"); - match reqwest::get("https://quorums.testnet.networks.dash.org/quorums").await { - Ok(resp) => debug!(status = %resp.status(), "dash_sdk_create_trusted: direct quorums endpoint test successful"), - Err(e) => warn!(error = %e, "dash_sdk_create_trusted: direct quorums endpoint test failed"), - } - - // Now try through the provider - match provider_for_prefetch.update_quorum_caches().await { - Ok(_) => info!("dash_sdk_create_trusted: successfully prefetched quorums"), - Err(e) => warn!(error = %e, "dash_sdk_create_trusted: failed to prefetch quorums; continuing"), - } - }); - - let wrapper = Box::new(SDKWrapper { - sdk, - runtime, - trusted_provider: Some(provider_for_wrapper), - }); - let handle = Box::into_raw(wrapper) as *mut SDKHandle; - DashSDKResult::success(handle as *mut std::os::raw::c_void) + spawn_trusted_quorum_prefetch(&runtime, trusted_provider); + make_sdk_result(sdk, runtime, Some(provider_for_wrapper)) } Err(e) => DashSDKResult::error(e.into()), } } -/// Create a new SDK instance with trusted setup and an explicit protocol version override +/// Create a new SDK instance /// -/// This creates an SDK with a trusted context provider that fetches quorum keys and -/// data contracts from trusted endpoints instead of requiring proof verification. +/// # Safety +/// - `config` must be a valid pointer to a DashSDKConfig structure for the duration of the call. +/// - The returned handle inside `DashSDKResult` must be destroyed using the SDK destroy function to avoid leaks. +#[no_mangle] +pub unsafe extern "C" fn dash_sdk_create(config: *const DashSDKConfig) -> DashSDKResult { + if config.is_null() { + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + "Config is null".to_string(), + )); + } + + create_sdk_from_config(&*config, None) +} + +/// Create a new SDK instance with an explicit protocol version override /// /// `protocol_version == 0` preserves the default auto-detect behavior. /// /// # Safety -/// - `config` must be a valid pointer to a DashSDKConfig structure -/// # Safety /// - `config` must be a valid pointer to a DashSDKConfig structure for the duration of the call. /// - The returned handle inside `DashSDKResult` must be destroyed using the SDK destroy function to avoid leaks. #[no_mangle] -pub unsafe extern "C" fn dash_sdk_create_trusted_with_protocol_version( +pub unsafe extern "C" fn dash_sdk_create_with_protocol_version( config: *const DashSDKConfig, protocol_version: u32, ) -> DashSDKResult { @@ -721,185 +436,110 @@ pub unsafe extern "C" fn dash_sdk_create_trusted_with_protocol_version( )); } - let config = &*config; - - // Parse configuration - let network: Network = config.network.into(); - let platform_version = match resolve_platform_version(protocol_version) { Ok(platform_version) => platform_version, Err(result) => return result, }; - // Use shared runtime - let runtime = match init_or_get_runtime() { - Ok(rt) => rt, - Err(e) => { - return DashSDKResult::error(DashSDKError::new(DashSDKErrorCode::InternalError, e)); - } - }; - - info!( - ?network, - "dash_sdk_create_trusted: creating trusted context provider" - ); + create_sdk_from_config(&*config, platform_version) +} - // Create trusted context provider - // For regtest, use the quorum sidecar at localhost:22444 (dashmate Docker default) - let is_local = matches!(network, Network::Regtest); - let trusted_provider = if is_local { - info!("dash_sdk_create_trusted: using local quorum sidecar for regtest"); - match rs_sdk_trusted_context_provider::TrustedHttpContextProvider::new_with_url( - network, - "http://127.0.0.1:22444".to_string(), - std::num::NonZeroUsize::new(100).unwrap(), - ) { - Ok(provider) => { - info!("dash_sdk_create_trusted: local trusted context provider created"); - Arc::new(provider) - } - Err(e) => { - error!(error = %e, "dash_sdk_create_trusted: failed to create local context provider"); - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InternalError, - format!("Failed to create local context provider: {}", e), - )); - } - } - } else { - match rs_sdk_trusted_context_provider::TrustedHttpContextProvider::new( - network, - None, // Use default quorum lookup endpoints - std::num::NonZeroUsize::new(100).unwrap(), // Cache size - ) { - Ok(provider) => { - info!("dash_sdk_create_trusted: trusted context provider created"); - Arc::new(provider) - } - Err(e) => { - error!(error = %e, "dash_sdk_create_trusted: failed to create trusted context provider"); - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InternalError, - format!("Failed to create trusted context provider: {}", e), - )); - } - } - }; +/// Create a new SDK instance with extended configuration including context provider +/// +/// # Safety +/// - `config` must be a valid pointer to a DashSDKConfigExtended structure for the duration of the call. +/// - Any embedded pointers (context_provider/core_sdk_handle) must be valid when non-null. +/// - The returned handle inside `DashSDKResult` must be destroyed using the SDK destroy function to avoid leaks. +#[no_mangle] +pub unsafe extern "C" fn dash_sdk_create_extended( + config: *const DashSDKConfigExtended, +) -> DashSDKResult { + if config.is_null() { + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + "Config is null".to_string(), + )); + } - // Parse DAPI addresses - for trusted setup, we always need real addresses - let builder = if config.dapi_addresses.is_null() { - info!("dash_sdk_create_trusted: no DAPI addresses provided, using defaults for network"); - // Use default addresses for the network - match network { - Network::Testnet => SdkBuilder::new_testnet(), - Network::Mainnet => SdkBuilder::new_mainnet(), - _ => { - error!( - ?network, - "dash_sdk_create_trusted: no DAPI addresses for network" - ); - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - format!("DAPI addresses not available for network: {:?}", network), - )); - } - } - } else { - let addresses_str = match unsafe { CStr::from_ptr(config.dapi_addresses) }.to_str() { - Ok(s) => s, - Err(e) => { - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - format!("Invalid DAPI addresses string: {}", e), - )) - } - }; + create_extended_sdk_from_config(&*config, None) +} - if addresses_str.is_empty() { - error!("dash_sdk_create_trusted: empty DAPI addresses provided"); - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - "DAPI addresses cannot be empty for trusted setup".to_string(), - )); - } else { - info!( - addresses = addresses_str, - "dash_sdk_create_trusted: using provided DAPI addresses" - ); - // Parse the address list - let address_list = match AddressList::from_str(addresses_str) { - Ok(list) => { - info!("dash_sdk_create_trusted: successfully parsed addresses"); - list - } - Err(e) => { - error!(error = %e, "dash_sdk_create_trusted: failed to parse addresses"); - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - format!("Failed to parse DAPI addresses: {}", e), - )); - } - }; - - SdkBuilder::new(address_list).with_network(network) - } - }; +/// Create a new SDK instance with extended configuration and an explicit protocol version override +/// +/// `protocol_version == 0` preserves the default auto-detect behavior. +/// +/// # Safety +/// - `config` must be a valid pointer to a DashSDKConfigExtended structure for the duration of the call. +/// - Any embedded pointers (context_provider/core_sdk_handle) must be valid when non-null. +/// - The returned handle inside `DashSDKResult` must be destroyed using the SDK destroy function to avoid leaks. +#[no_mangle] +pub unsafe extern "C" fn dash_sdk_create_extended_with_protocol_version( + config: *const DashSDKConfigExtended, + protocol_version: u32, +) -> DashSDKResult { + if config.is_null() { + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + "Config is null".to_string(), + )); + } - let builder = if let Some(platform_version) = platform_version { - builder.with_version(platform_version) - } else { - builder + let platform_version = match resolve_platform_version(protocol_version) { + Ok(platform_version) => platform_version, + Err(result) => return result, }; - // Clone trusted provider for prefetching quorums - let provider_for_prefetch = Arc::clone(&trusted_provider); - let provider_for_wrapper = Arc::clone(&trusted_provider); + create_extended_sdk_from_config(&*config, platform_version) +} - // Add trusted context provider - info!("dash_sdk_create_trusted: adding trusted context provider to builder"); - let builder = builder.with_context_provider(Arc::clone(&trusted_provider)); +/// Create a new SDK instance with trusted setup +/// +/// This creates an SDK with a trusted context provider that fetches quorum keys and +/// data contracts from trusted endpoints instead of requiring proof verification. +/// +/// # Safety +/// - `config` must be a valid pointer to a DashSDKConfig structure for the duration of the call. +/// - The returned handle inside `DashSDKResult` must be destroyed using the SDK destroy function to avoid leaks. +#[no_mangle] +pub unsafe extern "C" fn dash_sdk_create_trusted(config: *const DashSDKConfig) -> DashSDKResult { + if config.is_null() { + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + "Config is null".to_string(), + )); + } - // Build SDK - let sdk_result = builder.build().map_err(FFIError::from); + create_trusted_sdk_from_config(&*config, None) +} - match sdk_result { - Ok(sdk) => { - // Prefetch quorums for trusted setup - info!("dash_sdk_create_trusted: SDK built, prefetching quorums..."); - - let runtime_clone = runtime.handle().clone(); - runtime_clone.spawn(async move { - // First, try a simple HTTP test - debug!("dash_sdk_create_trusted: testing basic HTTP connectivity"); - match reqwest::get("https://www.google.com").await { - Ok(_) => debug!("dash_sdk_create_trusted: basic HTTP test successful (Google)"), - Err(e) => warn!(error = %e, "dash_sdk_create_trusted: basic HTTP test failed"), - } - - // Try the quorums endpoint directly - debug!("dash_sdk_create_trusted: testing quorums endpoint directly"); - match reqwest::get("https://quorums.testnet.networks.dash.org/quorums").await { - Ok(resp) => debug!(status = %resp.status(), "dash_sdk_create_trusted: direct quorums endpoint test successful"), - Err(e) => warn!(error = %e, "dash_sdk_create_trusted: direct quorums endpoint test failed"), - } - - // Now try through the provider - match provider_for_prefetch.update_quorum_caches().await { - Ok(_) => info!("dash_sdk_create_trusted: successfully prefetched quorums"), - Err(e) => warn!(error = %e, "dash_sdk_create_trusted: failed to prefetch quorums; continuing"), - } - }); - - let wrapper = Box::new(SDKWrapper { - sdk, - runtime, - trusted_provider: Some(provider_for_wrapper), - }); - let handle = Box::into_raw(wrapper) as *mut SDKHandle; - DashSDKResult::success(handle as *mut std::os::raw::c_void) - } - Err(e) => DashSDKResult::error(e.into()), +/// Create a new SDK instance with trusted setup and an explicit protocol version override +/// +/// This creates an SDK with a trusted context provider that fetches quorum keys and +/// data contracts from trusted endpoints instead of requiring proof verification. +/// +/// `protocol_version == 0` preserves the default auto-detect behavior. +/// +/// # Safety +/// - `config` must be a valid pointer to a DashSDKConfig structure for the duration of the call. +/// - The returned handle inside `DashSDKResult` must be destroyed using the SDK destroy function to avoid leaks. +#[no_mangle] +pub unsafe extern "C" fn dash_sdk_create_trusted_with_protocol_version( + config: *const DashSDKConfig, + protocol_version: u32, +) -> DashSDKResult { + if config.is_null() { + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + "Config is null".to_string(), + )); } + + let platform_version = match resolve_platform_version(protocol_version) { + Ok(platform_version) => platform_version, + Err(result) => return result, + }; + + create_trusted_sdk_from_config(&*config, platform_version) } /// Destroy an SDK instance @@ -1097,6 +737,7 @@ pub unsafe extern "C" fn dash_sdk_create_with_callbacks_and_protocol_version( } #[cfg(test)] +#[allow(clippy::items_after_test_module)] mod tests { use super::*; use crate::{ From db8a8db0dd146eb96bc28a52fd0f15427b722b61 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Mon, 25 May 2026 13:25:12 -0500 Subject: [PATCH 3/8] fix(rs-sdk-ffi): align sdk builder config paths --- packages/rs-sdk-ffi/src/context_callbacks.rs | 133 ++++++++++ packages/rs-sdk-ffi/src/sdk.rs | 249 +++++++++++++----- .../rs-sdk-ffi/tests/context_provider_test.rs | 10 +- 3 files changed, 328 insertions(+), 64 deletions(-) diff --git a/packages/rs-sdk-ffi/src/context_callbacks.rs b/packages/rs-sdk-ffi/src/context_callbacks.rs index d0fe9bd6aa0..2e3031bb93d 100644 --- a/packages/rs-sdk-ffi/src/context_callbacks.rs +++ b/packages/rs-sdk-ffi/src/context_callbacks.rs @@ -15,6 +15,8 @@ use dash_sdk::dpp::version::PlatformVersion; use dash_sdk::error::ContextProviderError; use drive_proof_verifier::ContextProvider; +use crate::context_provider::CoreSDKHandle; + /// Result type for FFI callbacks #[repr(C)] pub struct CallbackResult { @@ -93,6 +95,13 @@ pub struct CallbackContextProvider { callbacks: ContextProviderCallbacks, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CallbackContextProviderCreateError { + NullCoreSdkHandle, + NullCoreClientHandle, + MissingGlobalCallbacks, +} + impl CallbackContextProvider { /// Create a new callback-based context provider pub fn new(callbacks: ContextProviderCallbacks) -> Self { @@ -103,6 +112,27 @@ impl CallbackContextProvider { pub fn from_global() -> Option { get_global_callbacks().map(Self::new) } + + /// Create a callback-based provider using the globally registered callbacks + /// but replacing the callback handle with the provided Core SDK client handle. + pub fn from_core_sdk_handle( + core_sdk_handle: *mut CoreSDKHandle, + ) -> Result { + if core_sdk_handle.is_null() { + return Err(CallbackContextProviderCreateError::NullCoreSdkHandle); + } + + let core_sdk_handle = unsafe { &*core_sdk_handle }; + if core_sdk_handle.client.is_null() { + return Err(CallbackContextProviderCreateError::NullCoreClientHandle); + } + + let mut callbacks = get_global_callbacks() + .ok_or(CallbackContextProviderCreateError::MissingGlobalCallbacks)?; + callbacks.core_handle = core_sdk_handle.client; + + Ok(Self::new(callbacks)) + } } // SAFETY: CallbackContextProvider only contains function pointers and a handle @@ -187,3 +217,106 @@ impl ContextProvider for CallbackContextProvider { Ok(None) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicUsize, Ordering}; + + static LAST_CORE_HANDLE: AtomicUsize = AtomicUsize::new(0); + + extern "C" fn get_height_cb(handle: *mut c_void, out: *mut u32) -> CallbackResult { + LAST_CORE_HANDLE.store(handle as usize, Ordering::SeqCst); + unsafe { + if !out.is_null() { + *out = 42; + } + } + CallbackResult { + success: true, + error_code: 0, + error_message: std::ptr::null(), + } + } + + extern "C" fn get_quorum_pk_cb( + _handle: *mut c_void, + _quorum_type: u32, + _quorum_hash: *const u8, + _core_chain_locked_height: u32, + out: *mut u8, + ) -> CallbackResult { + unsafe { + if !out.is_null() { + std::ptr::write_bytes(out, 0, 48); + } + } + CallbackResult { + success: true, + error_code: 0, + error_message: std::ptr::null(), + } + } + + #[test] + fn from_core_sdk_handle_uses_client_handle_instead_of_global_handle() { + let global_handle = std::ptr::dangling_mut::(); + let core_client_handle = std::ptr::without_provenance_mut::(0x1234usize); + + unsafe { + set_global_callbacks(ContextProviderCallbacks { + core_handle: global_handle, + get_platform_activation_height: get_height_cb, + get_quorum_public_key: get_quorum_pk_cb, + }) + .expect("global callbacks should be set"); + } + + let mut core_sdk_handle = CoreSDKHandle { + client: core_client_handle, + }; + + let provider = CallbackContextProvider::from_core_sdk_handle(&mut core_sdk_handle) + .expect("provider should be created from core SDK handle"); + + let height = provider + .get_platform_activation_height() + .expect("callback should succeed"); + + assert_eq!(height, 42); + assert_eq!( + LAST_CORE_HANDLE.load(Ordering::SeqCst), + core_client_handle as usize + ); + assert_ne!( + LAST_CORE_HANDLE.load(Ordering::SeqCst), + global_handle as usize + ); + } + + #[test] + fn from_core_sdk_handle_rejects_null_client_handle() { + unsafe { + set_global_callbacks(ContextProviderCallbacks { + core_handle: std::ptr::dangling_mut::(), + get_platform_activation_height: get_height_cb, + get_quorum_public_key: get_quorum_pk_cb, + }) + .expect("global callbacks should be set"); + } + + let mut core_sdk_handle = CoreSDKHandle { + client: std::ptr::null_mut(), + }; + + let err = match CallbackContextProvider::from_core_sdk_handle(&mut core_sdk_handle) { + Ok(_) => panic!("null client handles must be rejected"), + Err(err) => err, + }; + + assert_eq!( + err, + CallbackContextProviderCreateError::NullCoreClientHandle + ); + } +} diff --git a/packages/rs-sdk-ffi/src/sdk.rs b/packages/rs-sdk-ffi/src/sdk.rs index 73f5c0da1b5..4a9c04473b2 100644 --- a/packages/rs-sdk-ffi/src/sdk.rs +++ b/packages/rs-sdk-ffi/src/sdk.rs @@ -7,9 +7,10 @@ use tracing::{debug, error, info, warn}; use dash_sdk::dpp::serialization::PlatformDeserializableWithPotentialValidationFromVersionedStructure; use dash_sdk::dpp::version::PlatformVersion; use dash_sdk::sdk::AddressList; -use dash_sdk::{Sdk, SdkBuilder}; +use dash_sdk::{RequestSettings, Sdk, SdkBuilder}; use std::ffi::CStr; use std::str::FromStr; +use std::time::Duration; use crate::context_provider::{ContextProviderHandle, ContextProviderWrapper, CoreSDKHandle}; use crate::types::{DashSDKConfig, FFINetwork, Network, SDKHandle}; @@ -141,7 +142,20 @@ fn build_sdk_builder(config: &DashSDKConfig) -> Result SdkBuilder::new_mock(), }; - Ok(builder.with_network(network)) + Ok(apply_common_builder_config( + builder.with_network(network), + config, + )) +} + +fn apply_common_builder_config(builder: SdkBuilder, config: &DashSDKConfig) -> SdkBuilder { + builder + .with_proofs(!config.skip_asset_lock_proof_verification) + .with_settings(RequestSettings { + timeout: Some(Duration::from_millis(config.request_timeout_ms)), + retries: Some(config.request_retry_count as usize), + ..RequestSettings::default() + }) } fn apply_platform_version( @@ -164,16 +178,30 @@ fn apply_context_provider( unsafe { &*(config.context_provider as *const ContextProviderWrapper) }; builder = builder.with_context_provider(provider_wrapper.provider()); } else if !config.core_sdk_handle.is_null() { - if let Some(callback_provider) = - crate::context_callbacks::CallbackContextProvider::from_global() - { - builder = builder.with_context_provider(callback_provider); - } else { - return Err(DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InternalError, - "Failed to create context provider. Make sure to call dash_sdk_register_context_callbacks first.".to_string(), - ))); - } + let callback_provider = + match crate::context_callbacks::CallbackContextProvider::from_core_sdk_handle( + config.core_sdk_handle, + ) { + Ok(callback_provider) => callback_provider, + Err( + crate::context_callbacks::CallbackContextProviderCreateError::NullCoreSdkHandle + | crate::context_callbacks::CallbackContextProviderCreateError::NullCoreClientHandle, + ) => { + return Err(DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + "Invalid core SDK handle: client pointer is null".to_string(), + ))); + } + Err( + crate::context_callbacks::CallbackContextProviderCreateError::MissingGlobalCallbacks, + ) => { + return Err(DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InternalError, + "Failed to create context provider. Make sure to call dash_sdk_register_context_callbacks first.".to_string(), + ))); + } + }; + builder = builder.with_context_provider(callback_provider); } else if let Some(callback_provider) = crate::context_callbacks::CallbackContextProvider::from_global() { @@ -300,59 +328,36 @@ fn build_trusted_sdk_builder( } }; - let builder = if config.dapi_addresses.is_null() { - info!("dash_sdk_create_trusted: no DAPI addresses provided, using defaults for network"); - match network { - Network::Testnet => SdkBuilder::new_testnet(), - Network::Mainnet => SdkBuilder::new_mainnet(), - _ => { - error!( - ?network, - "dash_sdk_create_trusted: no DAPI addresses for network" - ); - return Err(DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - format!("DAPI addresses not available for network: {:?}", network), - ))); + let builder = match parse_dapi_addresses(config)? { + None => { + info!( + "dash_sdk_create_trusted: no DAPI addresses provided, using defaults for network" + ); + match network { + Network::Testnet => SdkBuilder::new_testnet(), + Network::Mainnet => SdkBuilder::new_mainnet(), + _ => { + error!( + ?network, + "dash_sdk_create_trusted: no DAPI addresses for network" + ); + return Err(DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + format!("DAPI addresses not available for network: {:?}", network), + ))); + } } } - } else { - let addresses_str = unsafe { CStr::from_ptr(config.dapi_addresses) } - .to_str() - .map_err(|e| { - DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - format!("Invalid DAPI addresses string: {}", e), - )) - })?; - - if addresses_str.is_empty() { - error!("dash_sdk_create_trusted: empty DAPI addresses provided"); - return Err(DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - "DAPI addresses cannot be empty for trusted setup".to_string(), - ))); + Some(address_list) => { + info!("dash_sdk_create_trusted: using provided DAPI addresses"); + SdkBuilder::new(address_list).with_network(network) } - - info!( - addresses = addresses_str, - "dash_sdk_create_trusted: using provided DAPI addresses" - ); - let address_list = AddressList::from_str(addresses_str).map_err(|e| { - error!(error = %e, "dash_sdk_create_trusted: failed to parse addresses"); - DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - format!("Failed to parse DAPI addresses: {}", e), - )) - })?; - info!("dash_sdk_create_trusted: successfully parsed addresses"); - - SdkBuilder::new(address_list).with_network(network) }; info!("dash_sdk_create_trusted: adding trusted context provider to builder"); Ok(( - builder.with_context_provider(Arc::clone(&trusted_provider)), + apply_common_builder_config(builder, config) + .with_context_provider(Arc::clone(&trusted_provider)), trusted_provider, )) } @@ -741,9 +746,12 @@ pub unsafe extern "C" fn dash_sdk_create_with_callbacks_and_protocol_version( mod tests { use super::*; use crate::{ - dash_sdk_destroy, dash_sdk_error_free, dash_sdk_get_inner_sdk_ptr, DashSDKErrorCode, - FFINetwork, + context_callbacks::{set_global_callbacks, CallbackResult, ContextProviderCallbacks}, + dash_sdk_create_extended, dash_sdk_destroy, dash_sdk_error_free, + dash_sdk_get_inner_sdk_ptr, DashSDKErrorCode, FFINetwork, }; + use std::ffi::CString; + use std::os::raw::c_void; unsafe fn read_error_message(error: *mut crate::DashSDKError) -> String { std::ffi::CStr::from_ptr((*error).message) @@ -836,6 +844,127 @@ mod tests { dash_sdk_error_free(result.error); } } + + #[test] + fn build_sdk_builder_applies_proofs_and_request_settings() { + let config = DashSDKConfig { + network: FFINetwork::Testnet, + dapi_addresses: std::ptr::null(), + skip_asset_lock_proof_verification: true, + request_retry_count: 7, + request_timeout_ms: 1_234, + }; + + let builder = match build_sdk_builder(&config) { + Ok(builder) => builder, + Err(_) => panic!("builder should be created"), + }; + let sdk = builder.build().expect("mock SDK should build"); + + assert!(!sdk.prove()); + assert_eq!(sdk.query_settings().request_settings.retries, Some(7)); + assert_eq!( + sdk.query_settings().request_settings.timeout, + Some(Duration::from_millis(1_234)) + ); + } + + #[test] + fn build_trusted_sdk_builder_empty_addresses_use_network_defaults() { + let dapi_addresses = CString::new("").expect("cstring"); + let config = DashSDKConfig { + network: FFINetwork::Testnet, + dapi_addresses: dapi_addresses.as_ptr(), + skip_asset_lock_proof_verification: true, + request_retry_count: 4, + request_timeout_ms: 5_678, + }; + + let (builder, _trusted_provider) = match build_trusted_sdk_builder(&config) { + Ok(parts) => parts, + Err(_) => panic!("trusted builder should accept empty addresses"), + }; + let sdk = builder.build().expect("trusted SDK builder should build"); + + assert_eq!(sdk.network, Network::Testnet); + assert!(!sdk.prove()); + assert_eq!(sdk.query_settings().request_settings.retries, Some(4)); + assert_eq!( + sdk.query_settings().request_settings.timeout, + Some(Duration::from_millis(5_678)) + ); + } + + #[test] + fn dash_sdk_create_extended_rejects_null_core_client_handle() { + extern "C" fn get_height_cb(_h: *mut c_void, out: *mut u32) -> CallbackResult { + unsafe { + if !out.is_null() { + *out = 0; + } + } + CallbackResult { + success: true, + error_code: 0, + error_message: std::ptr::null(), + } + } + + extern "C" fn get_quorum_pk_cb( + _h: *mut c_void, + _qt: u32, + _qh: *const u8, + _hgt: u32, + out: *mut u8, + ) -> CallbackResult { + unsafe { + if !out.is_null() { + std::ptr::write_bytes(out, 0, 48); + } + } + CallbackResult { + success: true, + error_code: 0, + error_message: std::ptr::null(), + } + } + + unsafe { + set_global_callbacks(ContextProviderCallbacks { + core_handle: std::ptr::dangling_mut::(), + get_platform_activation_height: get_height_cb, + get_quorum_public_key: get_quorum_pk_cb, + }) + .expect("global callbacks should be set"); + } + + let mut core_sdk_handle = CoreSDKHandle { + client: std::ptr::null_mut(), + }; + let extended_config = DashSDKConfigExtended { + base_config: DashSDKConfig { + network: FFINetwork::Testnet, + dapi_addresses: std::ptr::null(), + skip_asset_lock_proof_verification: false, + request_retry_count: 3, + request_timeout_ms: 30_000, + }, + context_provider: std::ptr::null_mut(), + core_sdk_handle: &mut core_sdk_handle, + }; + + let result = unsafe { dash_sdk_create_extended(&extended_config) }; + + assert!(!result.error.is_null(), "expected invalid handle error"); + let error = unsafe { &*result.error }; + assert_eq!(error.code, DashSDKErrorCode::InvalidParameter); + let message = unsafe { read_error_message(result.error) }; + assert!(message.contains("Invalid core SDK handle")); + + unsafe { + dash_sdk_error_free(result.error); + } + } } /// Get the current network the SDK is connected to diff --git a/packages/rs-sdk-ffi/tests/context_provider_test.rs b/packages/rs-sdk-ffi/tests/context_provider_test.rs index 6cbdc2050c1..8c0cbb84ac7 100644 --- a/packages/rs-sdk-ffi/tests/context_provider_test.rs +++ b/packages/rs-sdk-ffi/tests/context_provider_test.rs @@ -73,9 +73,11 @@ mod tests { #[test] fn test_sdk_creation_with_context_provider() { unsafe { - // Create a mock Core SDK handle using an opaque pointer - // In real usage, this would come from the Core SDK - let core_handle_ptr = std::ptr::dangling_mut::(); + // Create a mock Core SDK handle using a real wrapper struct. + // The FFI path now validates and reads `core_sdk_handle.client`. + let mut core_handle = CoreSDKHandle { + client: std::ptr::dangling_mut::(), + }; // Create base config let dapi_addresses = CString::new("https://testnet.dash.org:3000").unwrap(); @@ -91,7 +93,7 @@ mod tests { let extended_config = DashSDKConfigExtended { base_config, context_provider: ptr::null_mut(), - core_sdk_handle: core_handle_ptr, + core_sdk_handle: &mut core_handle, }; // Create SDK with extended config From 687508ccc3c21c443b583d16eefca1c66ffe32f3 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Tue, 26 May 2026 23:15:34 -0500 Subject: [PATCH 4/8] fix(rs-sdk-ffi): address remaining PR3734 review feedback - Move `protocol_version` into `DashSDKConfigExtended` per Lukasz's request: `dash_sdk_create_extended` now reads the field directly (0 = auto-detect) and the redundant `dash_sdk_create_extended_with_protocol_version` constructor is removed. Consolidates the two `dash_sdk_create_with_callbacks` variants behind a shared inner helper that sets the new field. - Make the `GLOBAL_CALLBACKS` unit tests deterministic by adding a `test_support::lock_global_callbacks_for_test` guard that serializes test access and restores any prior callbacks on drop. - Mark `CallbackContextProvider::from_core_sdk_handle` `unsafe` (it dereferences a raw pointer) and document the contract; update call sites accordingly. - Clarify in code and docs that `skip_asset_lock_proof_verification` toggles `SdkBuilder::with_proofs` for *all* requests rather than asset-lock-specific proofs. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/rs-sdk-ffi/README.md | 8 +- packages/rs-sdk-ffi/src/context_callbacks.rs | 71 +++++- packages/rs-sdk-ffi/src/sdk.rs | 202 ++++++++++-------- packages/rs-sdk-ffi/src/types.rs | 9 +- .../rs-sdk-ffi/tests/context_provider_test.rs | 1 + 5 files changed, 193 insertions(+), 98 deletions(-) diff --git a/packages/rs-sdk-ffi/README.md b/packages/rs-sdk-ffi/README.md index e6971b44f12..d84e882ef4e 100644 --- a/packages/rs-sdk-ffi/README.md +++ b/packages/rs-sdk-ffi/README.md @@ -76,7 +76,13 @@ Build for your target platform using the appropriate Rust target. // Initialize the SDK dash_sdk_init(); -// Create SDK configuration +// Create SDK configuration. +// +// `skip_asset_lock_proof_verification`, despite its name, currently toggles +// Platform state-proof verification for *all* SDK requests (it is wired to +// `SdkBuilder::with_proofs`). Leave it `false` in production; set it to +// `true` only for testing or in trusted-context flows where proof +// verification is not desired. DashSDKConfig config = { .network = DASH_SDK_NETWORK_TESTNET, .dapi_addresses = "seed-1.testnet.networks.dash.org", diff --git a/packages/rs-sdk-ffi/src/context_callbacks.rs b/packages/rs-sdk-ffi/src/context_callbacks.rs index 2e3031bb93d..b6b73ccb994 100644 --- a/packages/rs-sdk-ffi/src/context_callbacks.rs +++ b/packages/rs-sdk-ffi/src/context_callbacks.rs @@ -115,7 +115,14 @@ impl CallbackContextProvider { /// Create a callback-based provider using the globally registered callbacks /// but replacing the callback handle with the provided Core SDK client handle. - pub fn from_core_sdk_handle( + /// + /// # Safety + /// `core_sdk_handle` must either be null or a valid, dereferenceable + /// pointer to a `CoreSDKHandle` for the duration of the call. The + /// inner `client` pointer is not dereferenced here; it is only stored + /// and later passed back to the registered callbacks, which are + /// responsible for validating it. + pub unsafe fn from_core_sdk_handle( core_sdk_handle: *mut CoreSDKHandle, ) -> Result { if core_sdk_handle.is_null() { @@ -218,6 +225,49 @@ impl ContextProvider for CallbackContextProvider { } } +/// Test-only utilities for callers (including other test modules in this +/// crate) that need to mutate the process-wide `GLOBAL_CALLBACKS` without +/// races. Wrapped in `cfg(test)` so it never ships in release builds. +#[cfg(test)] +pub(crate) mod test_support { + use super::*; + use std::sync::{Mutex, MutexGuard}; + + static TEST_LOCK: Mutex<()> = Mutex::new(()); + + /// Guard returned by `lock_global_callbacks_for_test`. Holds a + /// crate-wide test mutex so that tests touching `GLOBAL_CALLBACKS` + /// are serialized, and restores the previously installed callbacks + /// (or `None`) on drop so that subsequent tests start clean. + pub struct GlobalCallbacksTestGuard { + _lock: MutexGuard<'static, ()>, + previous: Option, + } + + impl Drop for GlobalCallbacksTestGuard { + fn drop(&mut self) { + let storage = GLOBAL_CALLBACKS.get_or_init(|| RwLock::new(None)); + if let Ok(mut guard) = storage.write() { + *guard = self.previous.take(); + } + } + } + + /// Acquire exclusive access to the global callback storage for the + /// duration of a test, snapshotting any previously installed callbacks + /// so they can be restored on guard drop. + pub fn lock_global_callbacks_for_test() -> GlobalCallbacksTestGuard { + let lock = TEST_LOCK + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + let previous = get_global_callbacks(); + GlobalCallbacksTestGuard { + _lock: lock, + previous, + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -260,6 +310,8 @@ mod tests { #[test] fn from_core_sdk_handle_uses_client_handle_instead_of_global_handle() { + let _guard = test_support::lock_global_callbacks_for_test(); + let global_handle = std::ptr::dangling_mut::(); let core_client_handle = std::ptr::without_provenance_mut::(0x1234usize); @@ -276,8 +328,10 @@ mod tests { client: core_client_handle, }; - let provider = CallbackContextProvider::from_core_sdk_handle(&mut core_sdk_handle) - .expect("provider should be created from core SDK handle"); + let provider = unsafe { + CallbackContextProvider::from_core_sdk_handle(&mut core_sdk_handle) + .expect("provider should be created from core SDK handle") + }; let height = provider .get_platform_activation_height() @@ -296,6 +350,8 @@ mod tests { #[test] fn from_core_sdk_handle_rejects_null_client_handle() { + let _guard = test_support::lock_global_callbacks_for_test(); + unsafe { set_global_callbacks(ContextProviderCallbacks { core_handle: std::ptr::dangling_mut::(), @@ -309,10 +365,11 @@ mod tests { client: std::ptr::null_mut(), }; - let err = match CallbackContextProvider::from_core_sdk_handle(&mut core_sdk_handle) { - Ok(_) => panic!("null client handles must be rejected"), - Err(err) => err, - }; + let err = + match unsafe { CallbackContextProvider::from_core_sdk_handle(&mut core_sdk_handle) } { + Ok(_) => panic!("null client handles must be rejected"), + Err(err) => err, + }; assert_eq!( err, diff --git a/packages/rs-sdk-ffi/src/sdk.rs b/packages/rs-sdk-ffi/src/sdk.rs index 4a9c04473b2..3985d0e212e 100644 --- a/packages/rs-sdk-ffi/src/sdk.rs +++ b/packages/rs-sdk-ffi/src/sdk.rs @@ -25,6 +25,14 @@ pub struct DashSDKConfigExtended { pub context_provider: *mut ContextProviderHandle, /// Optional Core SDK handle for automatic context provider creation pub core_sdk_handle: *mut CoreSDKHandle, + /// Optional Platform protocol version to pin the SDK to. + /// + /// `0` preserves the default auto-detect behavior; any non-zero value + /// must correspond to a known `PlatformVersion` or SDK creation fails + /// with `InvalidParameter`. Callers that zero-initialize the struct + /// (e.g., Swift's `DashSDKConfigExtended()` or C `{0}` initializer) + /// automatically inherit the auto-detect default. + pub protocol_version: u32, } type TrustedProvider = Arc; @@ -149,6 +157,11 @@ fn build_sdk_builder(config: &DashSDKConfig) -> Result SdkBuilder { + // NOTE: `skip_asset_lock_proof_verification` toggles `SdkBuilder::with_proofs`, + // which controls Platform state-proof verification for *all* SDK requests, not + // just asset-lock proofs. The field is named for the original use case + // (skipping asset-lock proofs in tests) but its effect is broader. See the + // field's Rustdoc on `DashSDKConfig`. builder .with_proofs(!config.skip_asset_lock_proof_verification) .with_settings(RequestSettings { @@ -178,10 +191,13 @@ fn apply_context_provider( unsafe { &*(config.context_provider as *const ContextProviderWrapper) }; builder = builder.with_context_provider(provider_wrapper.provider()); } else if !config.core_sdk_handle.is_null() { + // SAFETY: caller guarantees `core_sdk_handle` is either null or a + // valid `CoreSDKHandle` pointer per `dash_sdk_create_extended`'s + // safety contract; we have already checked it for null above. let callback_provider = - match crate::context_callbacks::CallbackContextProvider::from_core_sdk_handle( + match unsafe { crate::context_callbacks::CallbackContextProvider::from_core_sdk_handle( config.core_sdk_handle, - ) { + ) } { Ok(callback_provider) => callback_provider, Err( crate::context_callbacks::CallbackContextProviderCreateError::NullCoreSdkHandle @@ -449,38 +465,19 @@ pub unsafe extern "C" fn dash_sdk_create_with_protocol_version( create_sdk_from_config(&*config, platform_version) } -/// Create a new SDK instance with extended configuration including context provider +/// Create a new SDK instance with extended configuration including context provider. /// -/// # Safety -/// - `config` must be a valid pointer to a DashSDKConfigExtended structure for the duration of the call. -/// - Any embedded pointers (context_provider/core_sdk_handle) must be valid when non-null. -/// - The returned handle inside `DashSDKResult` must be destroyed using the SDK destroy function to avoid leaks. -#[no_mangle] -pub unsafe extern "C" fn dash_sdk_create_extended( - config: *const DashSDKConfigExtended, -) -> DashSDKResult { - if config.is_null() { - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - "Config is null".to_string(), - )); - } - - create_extended_sdk_from_config(&*config, None) -} - -/// Create a new SDK instance with extended configuration and an explicit protocol version override -/// -/// `protocol_version == 0` preserves the default auto-detect behavior. +/// Honors `config.protocol_version` (where `0` keeps the default auto-detect +/// behavior; any non-zero value pins Platform protocol version, returning +/// `InvalidParameter` if unknown). /// /// # Safety /// - `config` must be a valid pointer to a DashSDKConfigExtended structure for the duration of the call. /// - Any embedded pointers (context_provider/core_sdk_handle) must be valid when non-null. /// - The returned handle inside `DashSDKResult` must be destroyed using the SDK destroy function to avoid leaks. #[no_mangle] -pub unsafe extern "C" fn dash_sdk_create_extended_with_protocol_version( +pub unsafe extern "C" fn dash_sdk_create_extended( config: *const DashSDKConfigExtended, - protocol_version: u32, ) -> DashSDKResult { if config.is_null() { return DashSDKResult::error(DashSDKError::new( @@ -489,12 +486,13 @@ pub unsafe extern "C" fn dash_sdk_create_extended_with_protocol_version( )); } - let platform_version = match resolve_platform_version(protocol_version) { + let config_ref = &*config; + let platform_version = match resolve_platform_version(config_ref.protocol_version) { Ok(platform_version) => platform_version, Err(result) => return result, }; - create_extended_sdk_from_config(&*config, platform_version) + create_extended_sdk_from_config(config_ref, platform_version) } /// Create a new SDK instance with trusted setup @@ -604,17 +602,20 @@ pub unsafe extern "C" fn dash_sdk_register_context_callbacks( } } -/// Create a new SDK instance with explicit context callbacks +/// Internal helper used by `dash_sdk_create_with_callbacks*` entry points. /// -/// This is an alternative to registering global callbacks. The callbacks are used only for this SDK instance. +/// Wraps the caller-provided callbacks in a `ContextProviderWrapper`, builds +/// a `DashSDKConfigExtended` borrowing the same `dapi_addresses` pointer +/// (which is only read by `dash_sdk_create_extended` before returning), and +/// dispatches to the unified creation function. /// /// # Safety -/// - `config` must be a valid pointer to a DashSDKConfig structure -/// - `callbacks` must contain valid function pointers that remain valid for the lifetime of the SDK -#[no_mangle] -pub unsafe extern "C" fn dash_sdk_create_with_callbacks( +/// See `dash_sdk_create_with_callbacks` and +/// `dash_sdk_create_with_callbacks_and_protocol_version`. +unsafe fn dash_sdk_create_with_callbacks_inner( config: *const DashSDKConfig, callbacks: *const crate::context_callbacks::ContextProviderCallbacks, + protocol_version: u32, ) -> DashSDKResult { if config.is_null() { return DashSDKResult::error(DashSDKError::new( @@ -630,7 +631,6 @@ pub unsafe extern "C" fn dash_sdk_create_with_callbacks( )); } - // Create extended config with callback-based context provider let callbacks = &*callbacks; let context_provider = crate::context_callbacks::CallbackContextProvider::new( crate::context_callbacks::ContextProviderCallbacks { @@ -659,9 +659,9 @@ pub unsafe extern "C" fn dash_sdk_create_with_callbacks( }, context_provider: context_provider_handle, core_sdk_handle: std::ptr::null_mut(), + protocol_version, }; - // Use the extended creation function let result = dash_sdk_create_extended(&extended_config); // Reclaim the context provider wrapper - the SDK has already cloned what it needs @@ -671,6 +671,21 @@ pub unsafe extern "C" fn dash_sdk_create_with_callbacks( result } +/// Create a new SDK instance with explicit context callbacks +/// +/// This is an alternative to registering global callbacks. The callbacks are used only for this SDK instance. +/// +/// # Safety +/// - `config` must be a valid pointer to a DashSDKConfig structure +/// - `callbacks` must contain valid function pointers that remain valid for the lifetime of the SDK +#[no_mangle] +pub unsafe extern "C" fn dash_sdk_create_with_callbacks( + config: *const DashSDKConfig, + callbacks: *const crate::context_callbacks::ContextProviderCallbacks, +) -> DashSDKResult { + dash_sdk_create_with_callbacks_inner(config, callbacks, 0) +} + /// Create a new SDK instance with explicit context callbacks and an explicit protocol version override /// /// This is an alternative to registering global callbacks. The callbacks are used only for this SDK instance. @@ -686,59 +701,7 @@ pub unsafe extern "C" fn dash_sdk_create_with_callbacks_and_protocol_version( callbacks: *const crate::context_callbacks::ContextProviderCallbacks, protocol_version: u32, ) -> DashSDKResult { - if config.is_null() { - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - "Config is null".to_string(), - )); - } - - if callbacks.is_null() { - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - "Callbacks is null".to_string(), - )); - } - - // Create extended config with callback-based context provider - let callbacks = &*callbacks; - let context_provider = crate::context_callbacks::CallbackContextProvider::new( - crate::context_callbacks::ContextProviderCallbacks { - core_handle: callbacks.core_handle, - get_platform_activation_height: callbacks.get_platform_activation_height, - get_quorum_public_key: callbacks.get_quorum_public_key, - }, - ); - - let wrapper = Box::new(ContextProviderWrapper::new(context_provider)); - let context_provider_handle = Box::into_raw(wrapper) as *mut ContextProviderHandle; - - // Read fields from the caller's config individually rather than copying - // the struct (which would duplicate the raw `dapi_addresses` pointer). - // The pointer is only borrowed for the duration of this call; the - // downstream `dash_sdk_create_extended` reads and copies the string data - // immediately before returning. - let config_ref = &*config; - let extended_config = DashSDKConfigExtended { - base_config: DashSDKConfig { - network: config_ref.network, - dapi_addresses: config_ref.dapi_addresses, - skip_asset_lock_proof_verification: config_ref.skip_asset_lock_proof_verification, - request_retry_count: config_ref.request_retry_count, - request_timeout_ms: config_ref.request_timeout_ms, - }, - context_provider: context_provider_handle, - core_sdk_handle: std::ptr::null_mut(), - }; - - // Use the extended creation function - let result = dash_sdk_create_extended_with_protocol_version(&extended_config, protocol_version); - - // Reclaim the context provider wrapper - the SDK has already cloned what it needs - // via `provider_wrapper.provider()` inside `dash_sdk_create_extended`. - let _ = Box::from_raw(context_provider_handle as *mut ContextProviderWrapper); - - result + dash_sdk_create_with_callbacks_inner(config, callbacks, protocol_version) } #[cfg(test)] @@ -929,6 +892,8 @@ mod tests { } } + let _guard = crate::context_callbacks::test_support::lock_global_callbacks_for_test(); + unsafe { set_global_callbacks(ContextProviderCallbacks { core_handle: std::ptr::dangling_mut::(), @@ -951,6 +916,7 @@ mod tests { }, context_provider: std::ptr::null_mut(), core_sdk_handle: &mut core_sdk_handle, + protocol_version: 0, }; let result = unsafe { dash_sdk_create_extended(&extended_config) }; @@ -965,6 +931,64 @@ mod tests { dash_sdk_error_free(result.error); } } + + #[test] + fn dash_sdk_create_extended_pins_protocol_version_from_config_field() { + let extended_config = DashSDKConfigExtended { + base_config: DashSDKConfig { + network: FFINetwork::Testnet, + dapi_addresses: std::ptr::null(), + skip_asset_lock_proof_verification: false, + request_retry_count: 3, + request_timeout_ms: 30_000, + }, + context_provider: std::ptr::null_mut(), + core_sdk_handle: std::ptr::null_mut(), + protocol_version: 11, + }; + + let result = unsafe { dash_sdk_create_extended(&extended_config) }; + assert!(result.error.is_null(), "expected success"); + + let handle = result.data as *mut SDKHandle; + let inner_sdk_ptr = unsafe { dash_sdk_get_inner_sdk_ptr(handle) }; + assert!(!inner_sdk_ptr.is_null(), "expected inner sdk pointer"); + + let sdk = unsafe { &*(inner_sdk_ptr as *const dash_sdk::Sdk) }; + assert_eq!(sdk.protocol_version_number(), 11); + + unsafe { + dash_sdk_destroy(handle); + } + } + + #[test] + fn dash_sdk_create_extended_rejects_invalid_protocol_version_from_config_field() { + let extended_config = DashSDKConfigExtended { + base_config: DashSDKConfig { + network: FFINetwork::Testnet, + dapi_addresses: std::ptr::null(), + skip_asset_lock_proof_verification: false, + request_retry_count: 3, + request_timeout_ms: 30_000, + }, + context_provider: std::ptr::null_mut(), + core_sdk_handle: std::ptr::null_mut(), + protocol_version: u32::MAX, + }; + + let result = unsafe { dash_sdk_create_extended(&extended_config) }; + assert!(!result.error.is_null(), "expected invalid parameter error"); + + let error = unsafe { &*result.error }; + assert_eq!(error.code, DashSDKErrorCode::InvalidParameter); + let message = unsafe { read_error_message(result.error) }; + assert!(message.contains("Unknown protocol version")); + + unsafe { + dash_sdk_error_free(result.error); + } + } } /// Get the current network the SDK is connected to diff --git a/packages/rs-sdk-ffi/src/types.rs b/packages/rs-sdk-ffi/src/types.rs index ce5e7eb20a3..4bbb0fcf4dd 100644 --- a/packages/rs-sdk-ffi/src/types.rs +++ b/packages/rs-sdk-ffi/src/types.rs @@ -71,7 +71,14 @@ pub struct DashSDKConfig { /// This pointer is only read during the creation call; the string data is /// immediately copied into Rust-owned memory. pub dapi_addresses: *const c_char, - /// Skip asset lock proof verification (for testing) + /// When `true`, disables Platform state-proof verification for **all** + /// SDK requests (wired to [`SdkBuilder::with_proofs(false)`]). The field + /// name reflects its original use case (skipping asset-lock proofs in + /// tests), but its effect is broader: callers should treat it as a + /// global "skip proofs" switch and only enable it for testing or for + /// trusted-context flows where proof verification is not desired. + /// + /// [`SdkBuilder::with_proofs(false)`]: https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-sdk/src/sdk.rs pub skip_asset_lock_proof_verification: bool, /// Number of retries for failed requests pub request_retry_count: u32, diff --git a/packages/rs-sdk-ffi/tests/context_provider_test.rs b/packages/rs-sdk-ffi/tests/context_provider_test.rs index 8c0cbb84ac7..f085d1b57c4 100644 --- a/packages/rs-sdk-ffi/tests/context_provider_test.rs +++ b/packages/rs-sdk-ffi/tests/context_provider_test.rs @@ -94,6 +94,7 @@ mod tests { base_config, context_provider: ptr::null_mut(), core_sdk_handle: &mut core_handle, + protocol_version: 0, // 0 = auto-detect }; // Create SDK with extended config From 35b6d4049d16641c8163eeb8b328b3b492eec69f Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Wed, 27 May 2026 02:39:27 -0500 Subject: [PATCH 5/8] fix(rs-sdk-ffi): pin protocol version via extended config --- packages/rs-sdk-ffi/README.md | 28 ++- packages/rs-sdk-ffi/src/sdk.rs | 193 +----------------- .../swift-sdk/Sources/SwiftDashSDK/SDK.swift | 22 +- 3 files changed, 51 insertions(+), 192 deletions(-) diff --git a/packages/rs-sdk-ffi/README.md b/packages/rs-sdk-ffi/README.md index d84e882ef4e..344ff7ca65e 100644 --- a/packages/rs-sdk-ffi/README.md +++ b/packages/rs-sdk-ffi/README.md @@ -91,9 +91,15 @@ DashSDKConfig config = { .request_timeout_ms = 30000 }; +DashSDKConfigExtended extended_config = { + .base_config = config, + .context_provider = NULL, + .core_sdk_handle = NULL, + .protocol_version = 11, // 0 keeps the default auto-detect behavior +}; + // Create SDK instance pinned to Platform protocol version 11. -// Pass 0 to keep the default auto-detect behavior. -DashSDKResult result = dash_sdk_create_with_protocol_version(&config, 11); +DashSDKResult result = dash_sdk_create_extended(&extended_config); if (result.error) { // Handle error dash_sdk_error_free(result.error); @@ -143,6 +149,14 @@ class DashSDKConfig(Structure): ("request_timeout_ms", c_uint64) ] +class DashSDKConfigExtended(Structure): + _fields_ = [ + ("base_config", DashSDKConfig), + ("context_provider", c_void_p), + ("core_sdk_handle", c_void_p), + ("protocol_version", c_uint32), + ] + config = DashSDKConfig( network=1, # Testnet dapi_addresses=b"seed-1.testnet.networks.dash.org", @@ -153,7 +167,13 @@ config = DashSDKConfig( # Create SDK instance pinned to protocol version 11. # Pass 0 to keep the default auto-detect behavior. -result = lib.dash_sdk_create_with_protocol_version(byref(config), 11) +extended_config = DashSDKConfigExtended( + base_config=config, + context_provider=None, + core_sdk_handle=None, + protocol_version=11, +) +result = lib.dash_sdk_create_extended(byref(extended_config)) # ... handle result and use SDK ``` @@ -164,7 +184,7 @@ result = lib.dash_sdk_create_with_protocol_version(byref(config), 11) #### Core Functions - `dash_sdk_init()` - Initialize the FFI library - `dash_sdk_create()` - Create an SDK instance -- `dash_sdk_create_with_protocol_version()` - Create an SDK instance with optional protocol-version pinning +- `dash_sdk_create_extended()` - Create an SDK instance with extended configuration, including optional `protocol_version` pinning - `dash_sdk_destroy()` - Destroy an SDK instance - `dash_sdk_version()` - Get the SDK version diff --git a/packages/rs-sdk-ffi/src/sdk.rs b/packages/rs-sdk-ffi/src/sdk.rs index 3985d0e212e..1703fd7f2c1 100644 --- a/packages/rs-sdk-ffi/src/sdk.rs +++ b/packages/rs-sdk-ffi/src/sdk.rs @@ -438,33 +438,6 @@ pub unsafe extern "C" fn dash_sdk_create(config: *const DashSDKConfig) -> DashSD create_sdk_from_config(&*config, None) } -/// Create a new SDK instance with an explicit protocol version override -/// -/// `protocol_version == 0` preserves the default auto-detect behavior. -/// -/// # Safety -/// - `config` must be a valid pointer to a DashSDKConfig structure for the duration of the call. -/// - The returned handle inside `DashSDKResult` must be destroyed using the SDK destroy function to avoid leaks. -#[no_mangle] -pub unsafe extern "C" fn dash_sdk_create_with_protocol_version( - config: *const DashSDKConfig, - protocol_version: u32, -) -> DashSDKResult { - if config.is_null() { - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - "Config is null".to_string(), - )); - } - - let platform_version = match resolve_platform_version(protocol_version) { - Ok(platform_version) => platform_version, - Err(result) => return result, - }; - - create_sdk_from_config(&*config, platform_version) -} - /// Create a new SDK instance with extended configuration including context provider. /// /// Honors `config.protocol_version` (where `0` keeps the default auto-detect @@ -515,36 +488,6 @@ pub unsafe extern "C" fn dash_sdk_create_trusted(config: *const DashSDKConfig) - create_trusted_sdk_from_config(&*config, None) } -/// Create a new SDK instance with trusted setup and an explicit protocol version override -/// -/// This creates an SDK with a trusted context provider that fetches quorum keys and -/// data contracts from trusted endpoints instead of requiring proof verification. -/// -/// `protocol_version == 0` preserves the default auto-detect behavior. -/// -/// # Safety -/// - `config` must be a valid pointer to a DashSDKConfig structure for the duration of the call. -/// - The returned handle inside `DashSDKResult` must be destroyed using the SDK destroy function to avoid leaks. -#[no_mangle] -pub unsafe extern "C" fn dash_sdk_create_trusted_with_protocol_version( - config: *const DashSDKConfig, - protocol_version: u32, -) -> DashSDKResult { - if config.is_null() { - return DashSDKResult::error(DashSDKError::new( - DashSDKErrorCode::InvalidParameter, - "Config is null".to_string(), - )); - } - - let platform_version = match resolve_platform_version(protocol_version) { - Ok(platform_version) => platform_version, - Err(result) => return result, - }; - - create_trusted_sdk_from_config(&*config, platform_version) -} - /// Destroy an SDK instance /// # Safety /// - `handle` must be a valid pointer previously returned by this SDK and not yet destroyed. @@ -602,20 +545,17 @@ pub unsafe extern "C" fn dash_sdk_register_context_callbacks( } } -/// Internal helper used by `dash_sdk_create_with_callbacks*` entry points. +/// Create a new SDK instance with explicit context callbacks /// -/// Wraps the caller-provided callbacks in a `ContextProviderWrapper`, builds -/// a `DashSDKConfigExtended` borrowing the same `dapi_addresses` pointer -/// (which is only read by `dash_sdk_create_extended` before returning), and -/// dispatches to the unified creation function. +/// This is an alternative to registering global callbacks. The callbacks are used only for this SDK instance. /// /// # Safety -/// See `dash_sdk_create_with_callbacks` and -/// `dash_sdk_create_with_callbacks_and_protocol_version`. -unsafe fn dash_sdk_create_with_callbacks_inner( +/// - `config` must be a valid pointer to a DashSDKConfig structure +/// - `callbacks` must contain valid function pointers that remain valid for the lifetime of the SDK +#[no_mangle] +pub unsafe extern "C" fn dash_sdk_create_with_callbacks( config: *const DashSDKConfig, callbacks: *const crate::context_callbacks::ContextProviderCallbacks, - protocol_version: u32, ) -> DashSDKResult { if config.is_null() { return DashSDKResult::error(DashSDKError::new( @@ -659,7 +599,7 @@ unsafe fn dash_sdk_create_with_callbacks_inner( }, context_provider: context_provider_handle, core_sdk_handle: std::ptr::null_mut(), - protocol_version, + protocol_version: 0, }; let result = dash_sdk_create_extended(&extended_config); @@ -671,39 +611,6 @@ unsafe fn dash_sdk_create_with_callbacks_inner( result } -/// Create a new SDK instance with explicit context callbacks -/// -/// This is an alternative to registering global callbacks. The callbacks are used only for this SDK instance. -/// -/// # Safety -/// - `config` must be a valid pointer to a DashSDKConfig structure -/// - `callbacks` must contain valid function pointers that remain valid for the lifetime of the SDK -#[no_mangle] -pub unsafe extern "C" fn dash_sdk_create_with_callbacks( - config: *const DashSDKConfig, - callbacks: *const crate::context_callbacks::ContextProviderCallbacks, -) -> DashSDKResult { - dash_sdk_create_with_callbacks_inner(config, callbacks, 0) -} - -/// Create a new SDK instance with explicit context callbacks and an explicit protocol version override -/// -/// This is an alternative to registering global callbacks. The callbacks are used only for this SDK instance. -/// -/// `protocol_version == 0` preserves the default auto-detect behavior. -/// -/// # Safety -/// - `config` must be a valid pointer to a DashSDKConfig structure -/// - `callbacks` must contain valid function pointers that remain valid for the lifetime of the SDK -#[no_mangle] -pub unsafe extern "C" fn dash_sdk_create_with_callbacks_and_protocol_version( - config: *const DashSDKConfig, - callbacks: *const crate::context_callbacks::ContextProviderCallbacks, - protocol_version: u32, -) -> DashSDKResult { - dash_sdk_create_with_callbacks_inner(config, callbacks, protocol_version) -} - #[cfg(test)] #[allow(clippy::items_after_test_module)] mod tests { @@ -722,92 +629,6 @@ mod tests { .into_owned() } - #[test] - fn dash_sdk_create_with_protocol_version_pins_v11() { - let config = DashSDKConfig { - network: FFINetwork::Testnet, - dapi_addresses: std::ptr::null(), - skip_asset_lock_proof_verification: false, - request_retry_count: 3, - request_timeout_ms: 30_000, - }; - - let result = unsafe { dash_sdk_create_with_protocol_version(&config, 11) }; - assert!(result.error.is_null(), "expected success"); - - let handle = result.data as *mut SDKHandle; - let inner_sdk_ptr = unsafe { dash_sdk_get_inner_sdk_ptr(handle) }; - assert!(!inner_sdk_ptr.is_null(), "expected inner sdk pointer"); - - let sdk = unsafe { &*(inner_sdk_ptr as *const dash_sdk::Sdk) }; - assert_eq!(sdk.protocol_version_number(), 11); - assert_eq!(sdk.version().protocol_version, 11); - - unsafe { - dash_sdk_destroy(handle); - } - } - - #[test] - fn dash_sdk_create_with_protocol_version_rejects_invalid_protocol_version() { - let config = DashSDKConfig { - network: FFINetwork::Testnet, - dapi_addresses: std::ptr::null(), - skip_asset_lock_proof_verification: false, - request_retry_count: 3, - request_timeout_ms: 30_000, - }; - - let result = unsafe { dash_sdk_create_with_protocol_version(&config, u32::MAX) }; - assert!(!result.error.is_null(), "expected invalid parameter error"); - - let error = unsafe { &*result.error }; - assert_eq!(error.code, DashSDKErrorCode::InvalidParameter); - let message = unsafe { read_error_message(result.error) }; - assert!(message.contains("Unknown protocol version")); - - unsafe { - dash_sdk_error_free(result.error); - } - } - - #[test] - fn resolve_platform_version_can_pin_v11_for_trusted_builder_flow() { - let platform_version = match resolve_platform_version(11) { - Ok(Some(platform_version)) => platform_version, - Ok(None) => panic!("expected a pinned platform version"), - Err(_) => panic!("expected protocol version 11"), - }; - let sdk = SdkBuilder::new_mock() - .with_version(platform_version) - .build() - .expect("expected mock SDK build"); - - assert_eq!(sdk.protocol_version_number(), 11); - assert_eq!(sdk.version().protocol_version, 11); - } - - #[test] - fn dash_sdk_create_trusted_with_protocol_version_rejects_invalid_protocol_version() { - let config = DashSDKConfig { - network: FFINetwork::Testnet, - dapi_addresses: std::ptr::null(), - skip_asset_lock_proof_verification: false, - request_retry_count: 3, - request_timeout_ms: 30_000, - }; - - let result = unsafe { dash_sdk_create_trusted_with_protocol_version(&config, u32::MAX) }; - assert!(!result.error.is_null(), "expected invalid parameter error"); - - let error = unsafe { &*result.error }; - assert_eq!(error.code, DashSDKErrorCode::InvalidParameter); - - unsafe { - dash_sdk_error_free(result.error); - } - } - #[test] fn build_sdk_builder_applies_proofs_and_request_settings() { let config = DashSDKConfig { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift b/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift index fbf2cdf79ef..a1fa54c922c 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift @@ -124,10 +124,28 @@ public final class SDK: @unchecked Sendable { result = localAddresses.withCString { addressesCStr -> DashSDKResult in var mutableConfig = config mutableConfig.dapi_addresses = addressesCStr - return dash_sdk_create_trusted_with_protocol_version(&mutableConfig, protocolVersion) + if protocolVersion == 0 { + return dash_sdk_create_trusted(&mutableConfig) + } + + var extendedConfig = DashSDKConfigExtended() + extendedConfig.base_config = mutableConfig + extendedConfig.context_provider = nil + extendedConfig.core_sdk_handle = nil + extendedConfig.protocol_version = protocolVersion + return dash_sdk_create_extended(&extendedConfig) } } else { - result = dash_sdk_create_trusted_with_protocol_version(&config, protocolVersion) + if protocolVersion == 0 { + result = dash_sdk_create_trusted(&config) + } else { + var extendedConfig = DashSDKConfigExtended() + extendedConfig.base_config = config + extendedConfig.context_provider = nil + extendedConfig.core_sdk_handle = nil + extendedConfig.protocol_version = protocolVersion + result = dash_sdk_create_extended(&extendedConfig) + } } // Check for errors From 24eb5dfd86ba1d8f54c241aa55b93c88ade8d1b2 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Wed, 27 May 2026 02:49:29 -0500 Subject: [PATCH 6/8] fix(rs-sdk-ffi): pin protocol version via separate constructors Adding `protocol_version` to the exported `#[repr(C)]` `DashSDKConfigExtended` struct breaks ABI for existing callers. Drop the field and expose protocol-version pinning through dedicated exported constructors instead, leaving `dash_sdk_create_extended`'s ABI unchanged (auto-detect by default). New / restored entry points: - dash_sdk_create_with_protocol_version - dash_sdk_create_extended_with_protocol_version - dash_sdk_create_trusted_with_protocol_version - dash_sdk_create_with_callbacks_and_protocol_version Swift `SDK` initializer now calls `dash_sdk_create_trusted_with_protocol_version` directly; README C and Python examples document the separate functions; tests cover the new constructors and no longer rely on a config field. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/rs-sdk-ffi/README.md | 34 +-- packages/rs-sdk-ffi/src/sdk.rs | 266 ++++++++++++++++-- .../rs-sdk-ffi/tests/context_provider_test.rs | 1 - .../swift-sdk/Sources/SwiftDashSDK/SDK.swift | 22 +- 4 files changed, 247 insertions(+), 76 deletions(-) diff --git a/packages/rs-sdk-ffi/README.md b/packages/rs-sdk-ffi/README.md index 344ff7ca65e..6e688d09f9d 100644 --- a/packages/rs-sdk-ffi/README.md +++ b/packages/rs-sdk-ffi/README.md @@ -91,15 +91,9 @@ DashSDKConfig config = { .request_timeout_ms = 30000 }; -DashSDKConfigExtended extended_config = { - .base_config = config, - .context_provider = NULL, - .core_sdk_handle = NULL, - .protocol_version = 11, // 0 keeps the default auto-detect behavior -}; - // Create SDK instance pinned to Platform protocol version 11. -DashSDKResult result = dash_sdk_create_extended(&extended_config); +// Pass 0 to keep the default auto-detect behavior. +DashSDKResult result = dash_sdk_create_with_protocol_version(&config, 11); if (result.error) { // Handle error dash_sdk_error_free(result.error); @@ -149,14 +143,6 @@ class DashSDKConfig(Structure): ("request_timeout_ms", c_uint64) ] -class DashSDKConfigExtended(Structure): - _fields_ = [ - ("base_config", DashSDKConfig), - ("context_provider", c_void_p), - ("core_sdk_handle", c_void_p), - ("protocol_version", c_uint32), - ] - config = DashSDKConfig( network=1, # Testnet dapi_addresses=b"seed-1.testnet.networks.dash.org", @@ -167,13 +153,7 @@ config = DashSDKConfig( # Create SDK instance pinned to protocol version 11. # Pass 0 to keep the default auto-detect behavior. -extended_config = DashSDKConfigExtended( - base_config=config, - context_provider=None, - core_sdk_handle=None, - protocol_version=11, -) -result = lib.dash_sdk_create_extended(byref(extended_config)) +result = lib.dash_sdk_create_with_protocol_version(byref(config), 11) # ... handle result and use SDK ``` @@ -184,7 +164,13 @@ result = lib.dash_sdk_create_extended(byref(extended_config)) #### Core Functions - `dash_sdk_init()` - Initialize the FFI library - `dash_sdk_create()` - Create an SDK instance -- `dash_sdk_create_extended()` - Create an SDK instance with extended configuration, including optional `protocol_version` pinning +- `dash_sdk_create_with_protocol_version()` - Create an SDK instance with an optional Platform protocol-version pin (pass `0` to auto-detect) +- `dash_sdk_create_extended()` - Create an SDK instance with extended configuration (context provider, callbacks) +- `dash_sdk_create_extended_with_protocol_version()` - Like `dash_sdk_create_extended` plus an optional protocol-version pin +- `dash_sdk_create_trusted()` - Create an SDK instance with a trusted context provider +- `dash_sdk_create_trusted_with_protocol_version()` - Like `dash_sdk_create_trusted` plus an optional protocol-version pin +- `dash_sdk_create_with_callbacks()` - Create an SDK instance with per-instance context callbacks +- `dash_sdk_create_with_callbacks_and_protocol_version()` - Like `dash_sdk_create_with_callbacks` plus an optional protocol-version pin - `dash_sdk_destroy()` - Destroy an SDK instance - `dash_sdk_version()` - Get the SDK version diff --git a/packages/rs-sdk-ffi/src/sdk.rs b/packages/rs-sdk-ffi/src/sdk.rs index 1703fd7f2c1..c1edff0b1fb 100644 --- a/packages/rs-sdk-ffi/src/sdk.rs +++ b/packages/rs-sdk-ffi/src/sdk.rs @@ -25,14 +25,6 @@ pub struct DashSDKConfigExtended { pub context_provider: *mut ContextProviderHandle, /// Optional Core SDK handle for automatic context provider creation pub core_sdk_handle: *mut CoreSDKHandle, - /// Optional Platform protocol version to pin the SDK to. - /// - /// `0` preserves the default auto-detect behavior; any non-zero value - /// must correspond to a known `PlatformVersion` or SDK creation fails - /// with `InvalidParameter`. Callers that zero-initialize the struct - /// (e.g., Swift's `DashSDKConfigExtended()` or C `{0}` initializer) - /// automatically inherit the auto-detect default. - pub protocol_version: u32, } type TrustedProvider = Arc; @@ -438,11 +430,39 @@ pub unsafe extern "C" fn dash_sdk_create(config: *const DashSDKConfig) -> DashSD create_sdk_from_config(&*config, None) } +/// Create a new SDK instance with an explicit protocol version override. +/// +/// `protocol_version == 0` preserves the default auto-detect behavior; any +/// non-zero value must correspond to a known `PlatformVersion` or SDK +/// creation fails with `InvalidParameter`. +/// +/// # Safety +/// - `config` must be a valid pointer to a DashSDKConfig structure for the duration of the call. +/// - The returned handle inside `DashSDKResult` must be destroyed using the SDK destroy function to avoid leaks. +#[no_mangle] +pub unsafe extern "C" fn dash_sdk_create_with_protocol_version( + config: *const DashSDKConfig, + protocol_version: u32, +) -> DashSDKResult { + if config.is_null() { + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + "Config is null".to_string(), + )); + } + + let platform_version = match resolve_platform_version(protocol_version) { + Ok(platform_version) => platform_version, + Err(result) => return result, + }; + + create_sdk_from_config(&*config, platform_version) +} + /// Create a new SDK instance with extended configuration including context provider. /// -/// Honors `config.protocol_version` (where `0` keeps the default auto-detect -/// behavior; any non-zero value pins Platform protocol version, returning -/// `InvalidParameter` if unknown). +/// Uses the default auto-detect Platform protocol version. To pin a specific +/// version, use [`dash_sdk_create_extended_with_protocol_version`] instead. /// /// # Safety /// - `config` must be a valid pointer to a DashSDKConfigExtended structure for the duration of the call. @@ -459,13 +479,37 @@ pub unsafe extern "C" fn dash_sdk_create_extended( )); } - let config_ref = &*config; - let platform_version = match resolve_platform_version(config_ref.protocol_version) { + create_extended_sdk_from_config(&*config, None) +} + +/// Create a new SDK instance with extended configuration and an explicit protocol version override. +/// +/// `protocol_version == 0` preserves the default auto-detect behavior; any +/// non-zero value must correspond to a known `PlatformVersion` or SDK +/// creation fails with `InvalidParameter`. +/// +/// # Safety +/// - `config` must be a valid pointer to a DashSDKConfigExtended structure for the duration of the call. +/// - Any embedded pointers (context_provider/core_sdk_handle) must be valid when non-null. +/// - The returned handle inside `DashSDKResult` must be destroyed using the SDK destroy function to avoid leaks. +#[no_mangle] +pub unsafe extern "C" fn dash_sdk_create_extended_with_protocol_version( + config: *const DashSDKConfigExtended, + protocol_version: u32, +) -> DashSDKResult { + if config.is_null() { + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + "Config is null".to_string(), + )); + } + + let platform_version = match resolve_platform_version(protocol_version) { Ok(platform_version) => platform_version, Err(result) => return result, }; - create_extended_sdk_from_config(config_ref, platform_version) + create_extended_sdk_from_config(&*config, platform_version) } /// Create a new SDK instance with trusted setup @@ -488,6 +532,38 @@ pub unsafe extern "C" fn dash_sdk_create_trusted(config: *const DashSDKConfig) - create_trusted_sdk_from_config(&*config, None) } +/// Create a new SDK instance with trusted setup and an explicit protocol version override +/// +/// This creates an SDK with a trusted context provider that fetches quorum keys and +/// data contracts from trusted endpoints instead of requiring proof verification. +/// +/// `protocol_version == 0` preserves the default auto-detect behavior; any +/// non-zero value must correspond to a known `PlatformVersion` or SDK +/// creation fails with `InvalidParameter`. +/// +/// # Safety +/// - `config` must be a valid pointer to a DashSDKConfig structure for the duration of the call. +/// - The returned handle inside `DashSDKResult` must be destroyed using the SDK destroy function to avoid leaks. +#[no_mangle] +pub unsafe extern "C" fn dash_sdk_create_trusted_with_protocol_version( + config: *const DashSDKConfig, + protocol_version: u32, +) -> DashSDKResult { + if config.is_null() { + return DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + "Config is null".to_string(), + )); + } + + let platform_version = match resolve_platform_version(protocol_version) { + Ok(platform_version) => platform_version, + Err(result) => return result, + }; + + create_trusted_sdk_from_config(&*config, platform_version) +} + /// Destroy an SDK instance /// # Safety /// - `handle` must be a valid pointer previously returned by this SDK and not yet destroyed. @@ -545,17 +621,21 @@ pub unsafe extern "C" fn dash_sdk_register_context_callbacks( } } -/// Create a new SDK instance with explicit context callbacks +/// Internal helper used by `dash_sdk_create_with_callbacks*` entry points. /// -/// This is an alternative to registering global callbacks. The callbacks are used only for this SDK instance. +/// Wraps the caller-provided callbacks in a `ContextProviderWrapper`, builds +/// a `DashSDKConfigExtended` borrowing the same `dapi_addresses` pointer +/// (which is only read by the downstream creation function before +/// returning), and dispatches to the appropriate creation function based on +/// `protocol_version` (0 = auto-detect). /// /// # Safety -/// - `config` must be a valid pointer to a DashSDKConfig structure -/// - `callbacks` must contain valid function pointers that remain valid for the lifetime of the SDK -#[no_mangle] -pub unsafe extern "C" fn dash_sdk_create_with_callbacks( +/// See `dash_sdk_create_with_callbacks` and +/// `dash_sdk_create_with_callbacks_and_protocol_version`. +unsafe fn dash_sdk_create_with_callbacks_inner( config: *const DashSDKConfig, callbacks: *const crate::context_callbacks::ContextProviderCallbacks, + protocol_version: u32, ) -> DashSDKResult { if config.is_null() { return DashSDKResult::error(DashSDKError::new( @@ -586,7 +666,7 @@ pub unsafe extern "C" fn dash_sdk_create_with_callbacks( // Read fields from the caller's config individually rather than copying // the struct (which would duplicate the raw `dapi_addresses` pointer). // The pointer is only borrowed for the duration of this call; the - // downstream `dash_sdk_create_extended` reads and copies the string data + // downstream creation function reads and copies the string data // immediately before returning. let config_ref = &*config; let extended_config = DashSDKConfigExtended { @@ -599,18 +679,56 @@ pub unsafe extern "C" fn dash_sdk_create_with_callbacks( }, context_provider: context_provider_handle, core_sdk_handle: std::ptr::null_mut(), - protocol_version: 0, }; - let result = dash_sdk_create_extended(&extended_config); + let result = if protocol_version == 0 { + dash_sdk_create_extended(&extended_config) + } else { + dash_sdk_create_extended_with_protocol_version(&extended_config, protocol_version) + }; // Reclaim the context provider wrapper - the SDK has already cloned what it needs - // via `provider_wrapper.provider()` inside `dash_sdk_create_extended`. + // via `provider_wrapper.provider()` inside the extended creation function. let _ = Box::from_raw(context_provider_handle as *mut ContextProviderWrapper); result } +/// Create a new SDK instance with explicit context callbacks +/// +/// This is an alternative to registering global callbacks. The callbacks are used only for this SDK instance. +/// +/// # Safety +/// - `config` must be a valid pointer to a DashSDKConfig structure +/// - `callbacks` must contain valid function pointers that remain valid for the lifetime of the SDK +#[no_mangle] +pub unsafe extern "C" fn dash_sdk_create_with_callbacks( + config: *const DashSDKConfig, + callbacks: *const crate::context_callbacks::ContextProviderCallbacks, +) -> DashSDKResult { + dash_sdk_create_with_callbacks_inner(config, callbacks, 0) +} + +/// Create a new SDK instance with explicit context callbacks and an explicit protocol version override +/// +/// This is an alternative to registering global callbacks. The callbacks are used only for this SDK instance. +/// +/// `protocol_version == 0` preserves the default auto-detect behavior; any +/// non-zero value must correspond to a known `PlatformVersion` or SDK +/// creation fails with `InvalidParameter`. +/// +/// # Safety +/// - `config` must be a valid pointer to a DashSDKConfig structure +/// - `callbacks` must contain valid function pointers that remain valid for the lifetime of the SDK +#[no_mangle] +pub unsafe extern "C" fn dash_sdk_create_with_callbacks_and_protocol_version( + config: *const DashSDKConfig, + callbacks: *const crate::context_callbacks::ContextProviderCallbacks, + protocol_version: u32, +) -> DashSDKResult { + dash_sdk_create_with_callbacks_inner(config, callbacks, protocol_version) +} + #[cfg(test)] #[allow(clippy::items_after_test_module)] mod tests { @@ -737,7 +855,6 @@ mod tests { }, context_provider: std::ptr::null_mut(), core_sdk_handle: &mut core_sdk_handle, - protocol_version: 0, }; let result = unsafe { dash_sdk_create_extended(&extended_config) }; @@ -754,7 +871,56 @@ mod tests { } #[test] - fn dash_sdk_create_extended_pins_protocol_version_from_config_field() { + fn dash_sdk_create_with_protocol_version_pins_v11() { + let config = DashSDKConfig { + network: FFINetwork::Testnet, + dapi_addresses: std::ptr::null(), + skip_asset_lock_proof_verification: false, + request_retry_count: 3, + request_timeout_ms: 30_000, + }; + + let result = unsafe { dash_sdk_create_with_protocol_version(&config, 11) }; + assert!(result.error.is_null(), "expected success"); + + let handle = result.data as *mut SDKHandle; + let inner_sdk_ptr = unsafe { dash_sdk_get_inner_sdk_ptr(handle) }; + assert!(!inner_sdk_ptr.is_null(), "expected inner sdk pointer"); + + let sdk = unsafe { &*(inner_sdk_ptr as *const dash_sdk::Sdk) }; + assert_eq!(sdk.protocol_version_number(), 11); + assert_eq!(sdk.version().protocol_version, 11); + + unsafe { + dash_sdk_destroy(handle); + } + } + + #[test] + fn dash_sdk_create_with_protocol_version_rejects_invalid_protocol_version() { + let config = DashSDKConfig { + network: FFINetwork::Testnet, + dapi_addresses: std::ptr::null(), + skip_asset_lock_proof_verification: false, + request_retry_count: 3, + request_timeout_ms: 30_000, + }; + + let result = unsafe { dash_sdk_create_with_protocol_version(&config, u32::MAX) }; + assert!(!result.error.is_null(), "expected invalid parameter error"); + + let error = unsafe { &*result.error }; + assert_eq!(error.code, DashSDKErrorCode::InvalidParameter); + let message = unsafe { read_error_message(result.error) }; + assert!(message.contains("Unknown protocol version")); + + unsafe { + dash_sdk_error_free(result.error); + } + } + + #[test] + fn dash_sdk_create_extended_with_protocol_version_pins_v11() { let extended_config = DashSDKConfigExtended { base_config: DashSDKConfig { network: FFINetwork::Testnet, @@ -765,10 +931,10 @@ mod tests { }, context_provider: std::ptr::null_mut(), core_sdk_handle: std::ptr::null_mut(), - protocol_version: 11, }; - let result = unsafe { dash_sdk_create_extended(&extended_config) }; + let result = + unsafe { dash_sdk_create_extended_with_protocol_version(&extended_config, 11) }; assert!(result.error.is_null(), "expected success"); let handle = result.data as *mut SDKHandle; @@ -777,6 +943,7 @@ mod tests { let sdk = unsafe { &*(inner_sdk_ptr as *const dash_sdk::Sdk) }; assert_eq!(sdk.protocol_version_number(), 11); + assert_eq!(sdk.version().protocol_version, 11); unsafe { dash_sdk_destroy(handle); @@ -784,7 +951,7 @@ mod tests { } #[test] - fn dash_sdk_create_extended_rejects_invalid_protocol_version_from_config_field() { + fn dash_sdk_create_extended_with_protocol_version_rejects_invalid_protocol_version() { let extended_config = DashSDKConfigExtended { base_config: DashSDKConfig { network: FFINetwork::Testnet, @@ -795,10 +962,10 @@ mod tests { }, context_provider: std::ptr::null_mut(), core_sdk_handle: std::ptr::null_mut(), - protocol_version: u32::MAX, }; - let result = unsafe { dash_sdk_create_extended(&extended_config) }; + let result = + unsafe { dash_sdk_create_extended_with_protocol_version(&extended_config, u32::MAX) }; assert!(!result.error.is_null(), "expected invalid parameter error"); let error = unsafe { &*result.error }; @@ -810,6 +977,43 @@ mod tests { dash_sdk_error_free(result.error); } } + + #[test] + fn resolve_platform_version_can_pin_v11_for_trusted_builder_flow() { + let platform_version = match resolve_platform_version(11) { + Ok(Some(platform_version)) => platform_version, + Ok(None) => panic!("expected a pinned platform version"), + Err(_) => panic!("expected protocol version 11"), + }; + let sdk = SdkBuilder::new_mock() + .with_version(platform_version) + .build() + .expect("expected mock SDK build"); + + assert_eq!(sdk.protocol_version_number(), 11); + assert_eq!(sdk.version().protocol_version, 11); + } + + #[test] + fn dash_sdk_create_trusted_with_protocol_version_rejects_invalid_protocol_version() { + let config = DashSDKConfig { + network: FFINetwork::Testnet, + dapi_addresses: std::ptr::null(), + skip_asset_lock_proof_verification: false, + request_retry_count: 3, + request_timeout_ms: 30_000, + }; + + let result = unsafe { dash_sdk_create_trusted_with_protocol_version(&config, u32::MAX) }; + assert!(!result.error.is_null(), "expected invalid parameter error"); + + let error = unsafe { &*result.error }; + assert_eq!(error.code, DashSDKErrorCode::InvalidParameter); + + unsafe { + dash_sdk_error_free(result.error); + } + } } /// Get the current network the SDK is connected to diff --git a/packages/rs-sdk-ffi/tests/context_provider_test.rs b/packages/rs-sdk-ffi/tests/context_provider_test.rs index f085d1b57c4..8c0cbb84ac7 100644 --- a/packages/rs-sdk-ffi/tests/context_provider_test.rs +++ b/packages/rs-sdk-ffi/tests/context_provider_test.rs @@ -94,7 +94,6 @@ mod tests { base_config, context_provider: ptr::null_mut(), core_sdk_handle: &mut core_handle, - protocol_version: 0, // 0 = auto-detect }; // Create SDK with extended config diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift b/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift index a1fa54c922c..fbf2cdf79ef 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift @@ -124,28 +124,10 @@ public final class SDK: @unchecked Sendable { result = localAddresses.withCString { addressesCStr -> DashSDKResult in var mutableConfig = config mutableConfig.dapi_addresses = addressesCStr - if protocolVersion == 0 { - return dash_sdk_create_trusted(&mutableConfig) - } - - var extendedConfig = DashSDKConfigExtended() - extendedConfig.base_config = mutableConfig - extendedConfig.context_provider = nil - extendedConfig.core_sdk_handle = nil - extendedConfig.protocol_version = protocolVersion - return dash_sdk_create_extended(&extendedConfig) + return dash_sdk_create_trusted_with_protocol_version(&mutableConfig, protocolVersion) } } else { - if protocolVersion == 0 { - result = dash_sdk_create_trusted(&config) - } else { - var extendedConfig = DashSDKConfigExtended() - extendedConfig.base_config = config - extendedConfig.context_provider = nil - extendedConfig.core_sdk_handle = nil - extendedConfig.protocol_version = protocolVersion - result = dash_sdk_create_extended(&extendedConfig) - } + result = dash_sdk_create_trusted_with_protocol_version(&config, protocolVersion) } // Check for errors From 6c448d85755a760d92be6fbc3cbe554deedd3b37 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Wed, 27 May 2026 02:51:28 -0500 Subject: [PATCH 7/8] refactor(rs-sdk-ffi): delegate callbacks helper directly to extended creator `dash_sdk_create_with_callbacks_inner` was dispatching through the public `dash_sdk_create_extended` / `dash_sdk_create_extended_with_protocol_version` extern wrappers, paying an extra null-check on a stack-borrowed reference and a second dispatch hop. Call `create_extended_sdk_from_config` directly with a pre-resolved platform version. Resolving the version before the wrapper allocation also keeps an `InvalidParameter` early return from leaking the context-provider box. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/rs-sdk-ffi/src/sdk.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/rs-sdk-ffi/src/sdk.rs b/packages/rs-sdk-ffi/src/sdk.rs index c1edff0b1fb..137093a63ab 100644 --- a/packages/rs-sdk-ffi/src/sdk.rs +++ b/packages/rs-sdk-ffi/src/sdk.rs @@ -625,9 +625,9 @@ pub unsafe extern "C" fn dash_sdk_register_context_callbacks( /// /// Wraps the caller-provided callbacks in a `ContextProviderWrapper`, builds /// a `DashSDKConfigExtended` borrowing the same `dapi_addresses` pointer -/// (which is only read by the downstream creation function before -/// returning), and dispatches to the appropriate creation function based on -/// `protocol_version` (0 = auto-detect). +/// (which is only read by `create_extended_sdk_from_config` before +/// returning), and delegates to it with the resolved Platform version +/// (`protocol_version == 0` keeps the default auto-detect behavior). /// /// # Safety /// See `dash_sdk_create_with_callbacks` and @@ -651,6 +651,13 @@ unsafe fn dash_sdk_create_with_callbacks_inner( )); } + // Resolve the platform version before allocating the context-provider + // wrapper so an `InvalidParameter` early return doesn't leak the box. + let platform_version = match resolve_platform_version(protocol_version) { + Ok(platform_version) => platform_version, + Err(result) => return result, + }; + let callbacks = &*callbacks; let context_provider = crate::context_callbacks::CallbackContextProvider::new( crate::context_callbacks::ContextProviderCallbacks { @@ -681,11 +688,7 @@ unsafe fn dash_sdk_create_with_callbacks_inner( core_sdk_handle: std::ptr::null_mut(), }; - let result = if protocol_version == 0 { - dash_sdk_create_extended(&extended_config) - } else { - dash_sdk_create_extended_with_protocol_version(&extended_config, protocol_version) - }; + let result = create_extended_sdk_from_config(&extended_config, platform_version); // Reclaim the context provider wrapper - the SDK has already cloned what it needs // via `provider_wrapper.provider()` inside the extended creation function. From 56578298975d3450af6fde3a90ca5c79a5a10401 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Wed, 27 May 2026 03:06:34 -0500 Subject: [PATCH 8/8] docs(rs-sdk-ffi): fix protocol pinning examples --- packages/rs-sdk-ffi/README.md | 4 ++-- packages/rs-sdk-ffi/src/types.rs | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/rs-sdk-ffi/README.md b/packages/rs-sdk-ffi/README.md index 6e688d09f9d..c893cd8793d 100644 --- a/packages/rs-sdk-ffi/README.md +++ b/packages/rs-sdk-ffi/README.md @@ -85,7 +85,7 @@ dash_sdk_init(); // verification is not desired. DashSDKConfig config = { .network = DASH_SDK_NETWORK_TESTNET, - .dapi_addresses = "seed-1.testnet.networks.dash.org", + .dapi_addresses = "https://seed-1.testnet.networks.dash.org:1443", .skip_asset_lock_proof_verification = false, .request_retry_count = 3, .request_timeout_ms = 30000 @@ -145,7 +145,7 @@ class DashSDKConfig(Structure): config = DashSDKConfig( network=1, # Testnet - dapi_addresses=b"seed-1.testnet.networks.dash.org", + dapi_addresses=b"https://seed-1.testnet.networks.dash.org:1443", skip_asset_lock_proof_verification=False, request_retry_count=3, request_timeout_ms=30000 diff --git a/packages/rs-sdk-ffi/src/types.rs b/packages/rs-sdk-ffi/src/types.rs index 4bbb0fcf4dd..558771cdc42 100644 --- a/packages/rs-sdk-ffi/src/types.rs +++ b/packages/rs-sdk-ffi/src/types.rs @@ -72,13 +72,11 @@ pub struct DashSDKConfig { /// immediately copied into Rust-owned memory. pub dapi_addresses: *const c_char, /// When `true`, disables Platform state-proof verification for **all** - /// SDK requests (wired to [`SdkBuilder::with_proofs(false)`]). The field + /// SDK requests (wired to [`dash_sdk::SdkBuilder::with_proofs`]). The field /// name reflects its original use case (skipping asset-lock proofs in /// tests), but its effect is broader: callers should treat it as a /// global "skip proofs" switch and only enable it for testing or for /// trusted-context flows where proof verification is not desired. - /// - /// [`SdkBuilder::with_proofs(false)`]: https://github.com/dashpay/platform/blob/v3.1-dev/packages/rs-sdk/src/sdk.rs pub skip_asset_lock_proof_verification: bool, /// Number of retries for failed requests pub request_retry_count: u32,