Skip to content
Closed
57 changes: 29 additions & 28 deletions packages/rs-sdk-ffi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Comment on lines 86 to 92

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 5657829897: the C example now uses a full DAPI URI with scheme and port: https://seed-1.testnet.networks.dash.org:1443.


// 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);
Expand All @@ -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
Expand All @@ -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
)
Comment on lines 146 to 152

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 5657829897: the Python example now uses the same full DAPI URI form (https://seed-1.testnet.networks.dash.org:1443) so it satisfies AddressList::from_str parsing.


# 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
```

Expand All @@ -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

Expand Down
190 changes: 190 additions & 0 deletions packages/rs-sdk-ffi/src/context_callbacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -103,6 +112,34 @@ impl CallbackContextProvider {
pub fn from_global() -> Option<Self> {
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<Self, CallbackContextProviderCreateError> {
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
Expand Down Expand Up @@ -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<ContextProviderCallbacks>,
}

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::<c_void>();
let core_client_handle = std::ptr::without_provenance_mut::<c_void>(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::<c_void>(),
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
);
}
}
Loading
Loading