diff --git a/packages/rs-sdk-ffi/README.md b/packages/rs-sdk-ffi/README.md index 16eb502a99b..c893cd8793d 100644 --- a/packages/rs-sdk-ffi/README.md +++ b/packages/rs-sdk-ffi/README.md @@ -76,16 +76,24 @@ 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", + .dapi_addresses = "https://seed-1.testnet.networks.dash.org:1443", + .skip_asset_lock_proof_verification = false, .request_retry_count = 3, .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 +114,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 @@ -147,19 +138,22 @@ 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) ] 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 ) -# 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 +164,13 @@ 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 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/context_callbacks.rs b/packages/rs-sdk-ffi/src/context_callbacks.rs index d0fe9bd6aa0..b6b73ccb994 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,34 @@ 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. + /// + /// # 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() { + 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 +224,156 @@ impl ContextProvider for CallbackContextProvider { Ok(None) } } + +/// 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::*; + 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 _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); + + 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 = 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() + .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() { + let _guard = test_support::lock_global_callbacks_for_test(); + + 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 unsafe { 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 aa53fa8fcc9..137093a63ab 100644 --- a/packages/rs-sdk-ffi/src/sdk.rs +++ b/packages/rs-sdk-ffi/src/sdk.rs @@ -5,10 +5,12 @@ 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 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}; @@ -25,6 +27,8 @@ pub struct DashSDKConfigExtended { pub core_sdk_handle: *mut CoreSDKHandle, } +type TrustedProvider = Arc; + /// Internal SDK wrapper pub(crate) struct SDKWrapper { pub sdk: Sdk, @@ -88,109 +92,151 @@ fn init_or_get_runtime() -> Result, String> { Ok(arc) } -/// 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 resolve_platform_version( + protocol_version: u32, +) -> Result, DashSDKResult> { + if protocol_version == 0 { + return Ok(None); } - let config = &*config; + PlatformVersion::get(protocol_version) + .map(Some) + .map_err(|e| { + DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + format!("Unknown protocol version {}: {}", protocol_version, e), + )) + }) +} - // Parse configuration - let network: Network = config.network.into(); +fn parse_dapi_addresses(config: &DashSDKConfig) -> Result, DashSDKResult> { + if config.dapi_addresses.is_null() { + return Ok(None); + } - // Use shared runtime - let runtime = match init_or_get_runtime() { - Ok(rt) => rt, - Err(e) => { - return DashSDKResult::error(DashSDKError::new(DashSDKErrorCode::InternalError, e)); - } - }; + 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), + )) + })?; - // 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() { + return Ok(None); + } - 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), - )) - } - }; + AddressList::from_str(addresses_str).map(Some).map_err(|e| { + DashSDKResult::error(DashSDKError::new( + DashSDKErrorCode::InvalidParameter, + format!("Failed to parse DAPI addresses: {}", e), + )) + }) +} - SdkBuilder::new(address_list).with_network(network) - } +fn build_sdk_builder(config: &DashSDKConfig) -> Result { + let network: Network = config.network.into(); + let builder = match parse_dapi_addresses(config)? { + Some(address_list) => SdkBuilder::new(address_list), + None => SdkBuilder::new_mock(), }; - // Build SDK - let sdk_result = builder.build().map_err(FFIError::from); + Ok(apply_common_builder_config( + builder.with_network(network), + config, + )) +} - 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()), +fn apply_common_builder_config(builder: SdkBuilder, config: &DashSDKConfig) -> 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 { + timeout: Some(Duration::from_millis(config.request_timeout_ms)), + retries: Some(config.request_retry_count as usize), + ..RequestSettings::default() + }) +} + +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 } } -/// 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(), - )); +fn apply_context_provider( + mut builder: SdkBuilder, + config: &DashSDKConfigExtended, +) -> Result { + if !config.context_provider.is_null() { + 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() { + // 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 unsafe { 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() + { + builder = builder.with_context_provider(callback_provider); } - let config = &*config; - let base_config = &config.base_config; + Ok(builder) +} - // Parse configuration - let network: Network = base_config.network.into(); +fn make_sdk_result( + sdk: Sdk, + runtime: Arc, + trusted_provider: Option, +) -> DashSDKResult { + 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) +} - // Use shared runtime +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) => { @@ -198,107 +244,24 @@ pub unsafe extern "C" fn dash_sdk_create_extended( } }; - // 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) - } + let builder = match build_sdk_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 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()), } } -/// 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 - let network: Network = config.network.into(); - - // Use shared runtime +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) => { @@ -306,13 +269,33 @@ pub unsafe extern "C" fn dash_sdk_create_trusted(config: *const DashSDKConfig) - } }; + let builder = match build_sdk_builder(&config.base_config) { + Ok(builder) => builder, + Err(result) => return result, + }; + + let builder = apply_platform_version(builder, platform_version); + let builder = match apply_context_provider(builder, config) { + Ok(builder) => builder, + Err(result) => return result, + }; + + match builder.build().map_err(FFIError::from) { + Ok(sdk) => make_sdk_result(sdk, runtime, None), + Err(e) => DashSDKResult::error(e.into()), + } +} + +fn build_trusted_sdk_builder( + config: &DashSDKConfig, +) -> Result<(SdkBuilder, TrustedProvider), DashSDKResult> { + let network: Network = config.network.into(); + 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"); @@ -327,17 +310,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"); @@ -345,124 +328,242 @@ 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(), - _ => { - 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 { + let builder = match parse_dapi_addresses(config)? { + None => { info!( - addresses = addresses_str, - "dash_sdk_create_trusted: using provided DAPI addresses" + "dash_sdk_create_trusted: no DAPI addresses provided, using defaults for network" ); - // 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( + 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!("Failed to parse DAPI addresses: {}", e), - )); + format!("DAPI addresses not available for network: {:?}", network), + ))); } - }; - + } + } + Some(address_list) => { + info!("dash_sdk_create_trusted: using provided DAPI addresses"); 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); - - // 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(( + apply_common_builder_config(builder, config) + .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 { - 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"), - } + 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") + } + } + }); +} - // 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"), - } +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)); + } + }; - // 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) + 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) => { + 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 +/// +/// # 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; 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. +/// +/// 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. +/// - 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; 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, platform_version) +} + +/// 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(), + )); + } + + 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. @@ -520,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 `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 -/// - `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( @@ -546,7 +651,13 @@ pub unsafe extern "C" fn dash_sdk_create_with_callbacks( )); } - // Create extended config with callback-based context provider + // 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 { @@ -562,7 +673,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 { @@ -577,16 +688,337 @@ pub unsafe extern "C" fn dash_sdk_create_with_callbacks( core_sdk_handle: std::ptr::null_mut(), }; - // Use the extended creation function - let result = dash_sdk_create_extended(&extended_config); + 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 `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 { + use super::*; + use crate::{ + 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) + .to_string_lossy() + .into_owned() + } + + #[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(), + } + } + + let _guard = crate::context_callbacks::test_support::lock_global_callbacks_for_test(); + + 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); + } + } + + #[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 dash_sdk_create_extended_with_protocol_version_pins_v11() { + 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(), + }; + + 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; + 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_extended_with_protocol_version_rejects_invalid_protocol_version() { + 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(), + }; + + 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 }; + 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/rs-sdk-ffi/src/types.rs b/packages/rs-sdk-ffi/src/types.rs index ce5e7eb20a3..558771cdc42 100644 --- a/packages/rs-sdk-ffi/src/types.rs +++ b/packages/rs-sdk-ffi/src/types.rs @@ -71,7 +71,12 @@ 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 [`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. 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 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 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