Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
eed0f36
feat(sdk): add protocol compatibility checks and write admission
llbartekll Oct 21, 2026
4858199
refactor(sdk): make protocol observations advisory
llbartekll Oct 21, 2026
8524ae2
chore: integrate v5.0-dev for protocol advisory
llbartekll Oct 21, 2026
86355b0
chore: integrate v5.1-dev for current iOS SDK APIs
llbartekll Oct 21, 2026
7853fef
fix(sdk): address protocol observation and broadcast review findings
llbartekll Oct 22, 2026
ee0f72d
test(sdk): configure browser lifecycle context provider
llbartekll Oct 22, 2026
2022316
fix(sdk): retain broadcast ambiguity across retry attempts
llbartekll Oct 22, 2026
a0509ac
chore(sdk): preserve existing observer result type under clippy
llbartekll Oct 22, 2026
fbc06fe
chore: integrate v5.1 development target through async SPV startup
llbartekll Oct 22, 2026
2deb5ff
fix(sdk): preserve verified waits across combined submission helpers
llbartekll Oct 22, 2026
4707ce8
chore: integrate DPNS v3 development target
llbartekll Oct 22, 2026
74437b2
fix(sdk): preserve unconfirmed identity address top-ups
llbartekll Oct 22, 2026
ba33127
fix(sdk): preserve unconfirmed submission errors across ffi
llbartekll Oct 22, 2026
2aef9ba
fix(sdk): preserve unconfirmed wallet presentation
llbartekll Oct 22, 2026
c3ed1b2
fix(sdk): recognize prefixed DAPI processing refusals
llbartekll Oct 22, 2026
4fe9c9e
refactor(sdk): expose existing verified protocol observation without …
llbartekll Oct 23, 2026
5b89fe8
fix(swift-sdk): preserve empty epoch query results
llbartekll Oct 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

199 changes: 199 additions & 0 deletions packages/rs-sdk-ffi/src/protocol_version/queries/compatibility.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
//! Thin C bindings for the SDK-owned protocol compatibility lifecycle.

use crate::sdk::SDKWrapper;
use crate::types::SDKHandle;
use crate::{DashSDKError, DashSDKErrorCode, DashSDKResult, FFIError};
use dash_sdk::{
ProtocolCompatibility, ProtocolCompatibilityState, ProtocolCompatibilityUnavailableReason,
};
use serde::Serialize;
use std::ffi::CString;

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct CompatibilitySnapshot {
generation: u64,
status: &'static str,
verified_protocol_version: Option<u32>,
effective_protocol_version: u32,
maximum_supported_protocol_version: u32,
newer_protocol_available: bool,
unavailable_reason: Option<&'static str>,
}

impl From<ProtocolCompatibility> for CompatibilitySnapshot {
fn from(value: ProtocolCompatibility) -> Self {
let newer_protocol_available = value.newer_protocol_available();
let (status, unavailable_reason) = match value.state {
ProtocolCompatibilityState::Unavailable { reason } => (
"unavailable",
Some(match reason {
ProtocolCompatibilityUnavailableReason::NotChecked => "notChecked",
ProtocolCompatibilityUnavailableReason::TimedOut => "timedOut",
ProtocolCompatibilityUnavailableReason::CheckFailed => "checkFailed",
ProtocolCompatibilityUnavailableReason::ProofsDisabled => "proofsDisabled",
}),
),
ProtocolCompatibilityState::Checking => ("checking", None),
ProtocolCompatibilityState::Checked => ("checked", None),
};
Self {
generation: value.generation,
status,
verified_protocol_version: value.verified_protocol_version,
effective_protocol_version: value.effective_protocol_version,
maximum_supported_protocol_version: value.maximum_supported_protocol_version,
newer_protocol_available,
unavailable_reason,
}
}
}

fn json_result(value: &impl Serialize) -> DashSDKResult {
match serde_json::to_string(value)
.map_err(FFIError::from)
.and_then(|json| CString::new(json).map_err(FFIError::from))
{
Ok(json) => DashSDKResult::success_string(json.into_raw()),
Err(error) => DashSDKResult::error(error.into()),
}
}

fn invalid_parameter(message: &str) -> DashSDKResult {
DashSDKResult::error(DashSDKError::new(
DashSDKErrorCode::InvalidParameter,
message.to_owned(),
))
}

/// Mark an informational check as pending and return its generation.
/// This is synchronous, makes no network request, and never restricts operations.
///
/// # Safety
/// `sdk_handle` must be live and `out_generation` must point to writable u64 storage.
#[no_mangle]
pub unsafe extern "C" fn dash_sdk_begin_protocol_compatibility_check(
sdk_handle: *const SDKHandle,
out_generation: *mut u64,
) -> DashSDKResult {
if sdk_handle.is_null() || out_generation.is_null() {
return invalid_parameter("SDK handle or generation output is null");
}
let wrapper = &*(sdk_handle as *const SDKWrapper);
*out_generation = wrapper.sdk.begin_protocol_compatibility_check();
DashSDKResult::success(std::ptr::null_mut())
}

/// Run the Rust-owned bounded informational check for `generation`.
/// The returned snapshot can have a newer generation if this check was superseded.
/// Network/check failures are represented by the snapshot, not fabricated success.
///
/// # Safety
/// `sdk_handle` must remain live until the blocking call returns. Free the successful
/// JSON string with `dash_sdk_string_free`, or the error with `dash_sdk_error_free`.
#[no_mangle]
pub unsafe extern "C" fn dash_sdk_refresh_protocol_compatibility(
sdk_handle: *const SDKHandle,
generation: u64,
) -> DashSDKResult {
if sdk_handle.is_null() {
return invalid_parameter("SDK handle is null");
}
let wrapper = &*(sdk_handle as *const SDKWrapper);
let snapshot = wrapper
.runtime
.block_on(wrapper.sdk.refresh_protocol_compatibility(generation));
json_result(&CompatibilitySnapshot::from(snapshot))
}

/// Read clone-shared compatibility state without making a network request.
///
/// # Safety
/// `sdk_handle` must be live. Free the successful JSON string with
/// `dash_sdk_string_free`, or the error with `dash_sdk_error_free`.
#[no_mangle]
pub unsafe extern "C" fn dash_sdk_get_protocol_compatibility(
sdk_handle: *const SDKHandle,
) -> DashSDKResult {
if sdk_handle.is_null() {
return invalid_parameter("SDK handle is null");
}
let wrapper = &*(sdk_handle as *const SDKWrapper);
json_result(&CompatibilitySnapshot::from(
wrapper.sdk.protocol_compatibility(),
))
}

#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CStr;

#[test]
fn should_preserve_snapshot_status_versions_and_string_ownership() {
let result = json_result(&CompatibilitySnapshot::from(ProtocolCompatibility {
generation: 7,
state: ProtocolCompatibilityState::Checked,
verified_protocol_version: Some(99),
effective_protocol_version: 13,
maximum_supported_protocol_version: 14,
}));
assert!(result.error.is_null());
let text = unsafe { CStr::from_ptr(result.data.cast()) };
let value: serde_json::Value = serde_json::from_slice(text.to_bytes()).expect("snapshot");
assert_eq!(value["status"], "checked");
assert_eq!(value["verifiedProtocolVersion"], 99);
assert_eq!(value["generation"], 7);
assert_eq!(value["newerProtocolAvailable"], true);
unsafe { crate::dash_sdk_string_free(result.data.cast()) };
}

#[test]
fn should_reject_null_handles_without_touching_outputs() {
let mut generation = 42;
let results = unsafe {
[
dash_sdk_begin_protocol_compatibility_check(std::ptr::null(), &mut generation),
dash_sdk_refresh_protocol_compatibility(std::ptr::null(), 0),
dash_sdk_get_protocol_compatibility(std::ptr::null()),
]
};
assert_eq!(generation, 42);
for result in results {
assert!(!result.error.is_null());
assert_eq!(
unsafe { (*result.error).code },
DashSDKErrorCode::InvalidParameter
);
unsafe { crate::dash_sdk_error_free(result.error) };
}
}

#[test]
fn should_publish_pending_check_generation_synchronously() {
use crate::test_utils::test_utils::{create_mock_sdk_handle, destroy_mock_sdk_handle};
let handle = create_mock_sdk_handle();
unsafe {
let mut generation = 0;
let begin = dash_sdk_begin_protocol_compatibility_check(handle, &mut generation);
assert!(begin.error.is_null());
assert!(generation > 0);

let snapshot = dash_sdk_get_protocol_compatibility(handle);
assert!(snapshot.error.is_null());
let json: serde_json::Value =
serde_json::from_slice(CStr::from_ptr(snapshot.data.cast()).to_bytes())
.expect("snapshot");
assert_eq!(json["generation"], generation);
assert_eq!(json["status"], "checking");
assert_eq!(json["newerProtocolAvailable"], false);
crate::dash_sdk_string_free(snapshot.data.cast());

let invalid = dash_sdk_begin_protocol_compatibility_check(handle, std::ptr::null_mut());
assert!(!invalid.error.is_null());
assert_eq!((*invalid.error).code, DashSDKErrorCode::InvalidParameter);
crate::dash_sdk_error_free(invalid.error);
destroy_mock_sdk_handle(handle);
}
}
}
2 changes: 2 additions & 0 deletions packages/rs-sdk-ffi/src/protocol_version/queries/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// Protocol version queries
pub mod compatibility;
pub mod refresh;
pub mod upgrade_state;
pub mod upgrade_vote_status;

// Re-export all public functions for convenient access
pub use compatibility::*;
pub use refresh::dash_sdk_refresh_protocol_version;
pub use upgrade_state::dash_sdk_protocol_version_get_upgrade_state;
pub use upgrade_vote_status::dash_sdk_protocol_version_get_upgrade_vote_status;
1 change: 1 addition & 0 deletions packages/rs-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ tokio = { version = "1.40", features = ["macros", "time", "rt-multi-thread"] }

[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = "0.3.64"
web-time = "1.1"

[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
tempfile = "3"
Expand Down
40 changes: 40 additions & 0 deletions packages/rs-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,43 @@ in `packages/rs-dapi-client/src/transport/grpc.rs`.
Running without arguments regenerates **all** vectors — avoid this unless intentional.
The script executes matching tests with `--features generate-test-vectors` against the running devnet,
saving responses as test vectors in `packages/rs-sdk/tests/vectors/`.

## Advisory protocol compatibility checks

`refresh_protocol_version()` retains its existing best-effort behavior. The
compatibility API provides authenticated observations for an application update
notice. Its check status never enables or disables wallet operations.

```rust,no_run
# async fn example(sdk: &dash_sdk::Sdk) {
let attempt = sdk.begin_protocol_compatibility_check();
let compatibility = sdk.refresh_protocol_compatibility(attempt).await;
if compatibility.newer_protocol_available() {
// Show an informational update notice; wallet operations remain available.
}
# }
```

`state` describes the latest explicit check: `Checking`, `Checked`, or
`Unavailable { reason }`. `verified_protocol_version` is the highest accepted
verified version observed during this SDK instance's lifetime, including normal
proved reads. It remains available after a later failed or in-progress check.
`effective_protocol_version` identifies the supported version currently used by
the SDK; it is separate from the observed network version. SDK clones share the
observations and check status.

The total check budget is 20 seconds from `begin`, including worker-queue delay.
The lifecycle uses a monotonic clock and the transport's timer on both native
and browser WASM targets; browsers do not require a Tokio timer runtime.
One proved genesis-epoch query supplies the network version through the normal
proof, quorum-signature and freshness verification path. Transport retries use
the existing configuration within that total budget. A superseded attempt's
late result is discarded. Applications can coalesce duplicate refresh requests
and check at startup, foreground return, or manual retry; operations do not need
a compatibility refresh before each request.

A failed proof, unavailable quorum key or timeout only makes the latest check
unavailable. An update notice requires an accepted verified version numerically
higher than `maximum_supported_protocol_version`; a failed check alone provides
no such evidence. Normal operation-specific validation and error reporting are
unchanged.
5 changes: 4 additions & 1 deletion packages/rs-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ pub mod platform;
pub mod sdk;

pub use error::Error;
pub use sdk::{RequestSettings, Sdk, SdkBuilder};
pub use sdk::{
ProtocolCompatibility, ProtocolCompatibilityState, ProtocolCompatibilityUnavailableReason,
RequestSettings, Sdk, SdkBuilder,
};

pub use dapi_grpc;
pub use dash_platform_queries;
Expand Down
Loading
Loading