From 45a8c4541bd658eacfd6e2edc9e55415947e53fc Mon Sep 17 00:00:00 2001
From: Lukasz Klimek <842586+lklimek@users.noreply.github.com>
Date: Mon, 15 Jun 2026 15:58:03 +0200
Subject: [PATCH 1/2] feat(sdk): refresh protocol version on SDK init
(FFI/WASM/JS)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The SDK seeds its protocol version to the per-network min_protocol_version
floor at construction and only ratchets upward once a proven query returns.
Wire an eager, best-effort refresh_protocol_version() into every real
(network-backed) init path so fee-sensitive flows reserve against the
network's actual protocol version from the first request instead of the
seed floor.
Per init path:
FFI (packages/rs-sdk-ffi/src/sdk.rs):
- dash_sdk_create -> refresh on the real (non-mock) branch
- dash_sdk_create_extended -> refresh on the real (non-mock) branch
- dash_sdk_create_trusted -> refresh always (path is always real)
- dash_sdk_create_with_callbacks -> covered indirectly (delegates to
dash_sdk_create_extended)
- dash_sdk_create_handle_with_mock -> skipped (mock only, no network)
WASM (packages/wasm-sdk/src/sdk.rs):
- WasmSdkBuilder::build is now async and awaits a best-effort refresh
before returning; mock/test constructors are unaffected.
JS (packages/js-evo-sdk/src/sdk.ts):
- EvoSDK::connect awaits the now-async builder.build().
Failures never abort SDK creation: a private best_effort_refresh helper
(FFI) and the WASM build path log at warn and proceed with the floor
version. Pinned SDKs are unaffected — refresh_protocol_version is a no-op
when version updating is disabled.
🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent
Co-Authored-By: Claude Opus 4.6
---
packages/js-evo-sdk/src/sdk.ts | 2 +-
packages/rs-sdk-ffi/src/sdk.rs | 37 ++++++++++++++++++++++++++--------
packages/wasm-sdk/src/sdk.rs | 8 +++++++-
3 files changed, 37 insertions(+), 10 deletions(-)
diff --git a/packages/js-evo-sdk/src/sdk.ts b/packages/js-evo-sdk/src/sdk.ts
index 9cf8aba176f..82d77670c9e 100644
--- a/packages/js-evo-sdk/src/sdk.ts
+++ b/packages/js-evo-sdk/src/sdk.ts
@@ -200,7 +200,7 @@ export class EvoSDK {
);
}
- this.wasmSdk = builder.build();
+ this.wasmSdk = await builder.build();
}
static fromWasm(wasmSdk: wasm.WasmSdk): EvoSDK {
diff --git a/packages/rs-sdk-ffi/src/sdk.rs b/packages/rs-sdk-ffi/src/sdk.rs
index 0f3f89b666b..711aa4d8d57 100644
--- a/packages/rs-sdk-ffi/src/sdk.rs
+++ b/packages/rs-sdk-ffi/src/sdk.rs
@@ -26,6 +26,19 @@ pub struct DashSDKConfigExtended {
pub core_sdk_handle: *mut CoreSDKHandle,
}
+/// Best-effort protocol-version refresh on SDK init.
+///
+/// Logs a warning and proceeds if the network is unreachable; never fails SDK creation.
+fn best_effort_refresh(sdk: &Sdk, runtime: &BigStackRuntime) {
+ match runtime.block_on(sdk.refresh_protocol_version()) {
+ Ok(v) => debug!(protocol_version = v, "protocol version refreshed on init"),
+ Err(e) => warn!(
+ error = %e,
+ "protocol version refresh failed on init; proceeding with floor version"
+ ),
+ }
+}
+
fn apply_version(builder: SdkBuilder, platform_version: u32) -> Result {
if platform_version == 0 {
return Ok(builder);
@@ -125,9 +138,9 @@ pub unsafe extern "C" fn dash_sdk_create(config: *const DashSDKConfig) -> DashSD
};
// Parse DAPI addresses
- let builder = if config.dapi_addresses.is_null() {
+ let (builder, is_real) = if config.dapi_addresses.is_null() {
// Use mock SDK if no addresses provided
- SdkBuilder::new_mock().with_network(network)
+ (SdkBuilder::new_mock().with_network(network), false)
} else {
let addresses_str = match unsafe { CStr::from_ptr(config.dapi_addresses) }.to_str() {
Ok(s) => s,
@@ -141,7 +154,7 @@ pub unsafe extern "C" fn dash_sdk_create(config: *const DashSDKConfig) -> DashSD
if addresses_str.is_empty() {
// Use mock SDK if addresses string is empty
- SdkBuilder::new_mock().with_network(network)
+ (SdkBuilder::new_mock().with_network(network), false)
} else {
// Parse the address list
let address_list = match AddressList::from_str(addresses_str) {
@@ -154,7 +167,7 @@ pub unsafe extern "C" fn dash_sdk_create(config: *const DashSDKConfig) -> DashSD
}
};
- SdkBuilder::new(address_list).with_network(network)
+ (SdkBuilder::new(address_list).with_network(network), true)
}
};
@@ -168,6 +181,9 @@ pub unsafe extern "C" fn dash_sdk_create(config: *const DashSDKConfig) -> DashSD
match sdk_result {
Ok(sdk) => {
+ if is_real {
+ best_effort_refresh(&sdk, &runtime);
+ }
// Clone Arc into the wrapper
let wrapper = Box::new(SDKWrapper {
sdk,
@@ -213,9 +229,9 @@ pub unsafe extern "C" fn dash_sdk_create_extended(
};
// Parse DAPI addresses
- let mut builder = if base_config.dapi_addresses.is_null() {
+ let (mut builder, is_real) = if base_config.dapi_addresses.is_null() {
// Use mock SDK if no addresses provided
- SdkBuilder::new_mock().with_network(network)
+ (SdkBuilder::new_mock().with_network(network), false)
} else {
let addresses_str = match unsafe { CStr::from_ptr(base_config.dapi_addresses) }.to_str() {
Ok(s) => s,
@@ -229,7 +245,7 @@ pub unsafe extern "C" fn dash_sdk_create_extended(
if addresses_str.is_empty() {
// Use mock SDK if addresses string is empty
- SdkBuilder::new_mock().with_network(network)
+ (SdkBuilder::new_mock().with_network(network), false)
} else {
// Parse the address list
let address_list = match AddressList::from_str(addresses_str) {
@@ -242,7 +258,7 @@ pub unsafe extern "C" fn dash_sdk_create_extended(
}
};
- SdkBuilder::new(address_list).with_network(network)
+ (SdkBuilder::new(address_list).with_network(network), true)
}
};
@@ -281,6 +297,9 @@ pub unsafe extern "C" fn dash_sdk_create_extended(
match sdk_result {
Ok(sdk) => {
+ if is_real {
+ best_effort_refresh(&sdk, &runtime);
+ }
let wrapper = Box::new(SDKWrapper {
sdk,
runtime,
@@ -487,6 +506,8 @@ pub unsafe extern "C" fn dash_sdk_create_trusted(config: *const DashSDKConfig) -
match sdk_result {
Ok(sdk) => {
+ best_effort_refresh(&sdk, &runtime);
+
// Prefetch quorums for trusted setup
info!("dash_sdk_create_trusted: SDK built, prefetching quorums...");
diff --git a/packages/wasm-sdk/src/sdk.rs b/packages/wasm-sdk/src/sdk.rs
index 57801f38475..a71fb5818ba 100644
--- a/packages/wasm-sdk/src/sdk.rs
+++ b/packages/wasm-sdk/src/sdk.rs
@@ -312,8 +312,14 @@ impl WasmSdkBuilder {
}
}
- pub fn build(self) -> Result {
+ pub async fn build(self) -> Result {
let sdk = self.inner.build().map_err(WasmSdkError::from)?;
+ if let Err(e) = sdk.refresh_protocol_version().await {
+ tracing::warn!(
+ error = %e,
+ "protocol version refresh failed on init; proceeding with floor version"
+ );
+ }
Ok(WasmSdk {
sdk,
trusted_context: self.trusted_context,
From 17428831c58d9cf289c58c240e99858082e3af99 Mon Sep 17 00:00:00 2001
From: Lukasz Klimek <842586+lklimek@users.noreply.github.com>
Date: Mon, 15 Jun 2026 16:31:01 +0200
Subject: [PATCH 2/2] refactor(sdk-ffi): self-skip mock refresh via
Sdk::is_mock, drop is_real bool
best_effort_refresh now checks Sdk::is_mock() and skips the proven refresh
on mock SDKs, so the FFI init paths no longer thread a redundant is_real
flag. Behaviour is unchanged: real SDKs refresh, mocks skip.
Co-Authored-By: Claude Opus 4.6
---
packages/rs-sdk-ffi/src/sdk.rs | 28 ++++++++++++++--------------
packages/rs-sdk/src/sdk.rs | 16 ++++++++++++++++
2 files changed, 30 insertions(+), 14 deletions(-)
diff --git a/packages/rs-sdk-ffi/src/sdk.rs b/packages/rs-sdk-ffi/src/sdk.rs
index 711aa4d8d57..c0c19209288 100644
--- a/packages/rs-sdk-ffi/src/sdk.rs
+++ b/packages/rs-sdk-ffi/src/sdk.rs
@@ -30,6 +30,10 @@ pub struct DashSDKConfigExtended {
///
/// Logs a warning and proceeds if the network is unreachable; never fails SDK creation.
fn best_effort_refresh(sdk: &Sdk, runtime: &BigStackRuntime) {
+ // Mock SDKs have no live network; refreshing only logs noise.
+ if sdk.is_mock() {
+ return;
+ }
match runtime.block_on(sdk.refresh_protocol_version()) {
Ok(v) => debug!(protocol_version = v, "protocol version refreshed on init"),
Err(e) => warn!(
@@ -138,9 +142,9 @@ pub unsafe extern "C" fn dash_sdk_create(config: *const DashSDKConfig) -> DashSD
};
// Parse DAPI addresses
- let (builder, is_real) = if config.dapi_addresses.is_null() {
+ let builder = if config.dapi_addresses.is_null() {
// Use mock SDK if no addresses provided
- (SdkBuilder::new_mock().with_network(network), false)
+ SdkBuilder::new_mock().with_network(network)
} else {
let addresses_str = match unsafe { CStr::from_ptr(config.dapi_addresses) }.to_str() {
Ok(s) => s,
@@ -154,7 +158,7 @@ pub unsafe extern "C" fn dash_sdk_create(config: *const DashSDKConfig) -> DashSD
if addresses_str.is_empty() {
// Use mock SDK if addresses string is empty
- (SdkBuilder::new_mock().with_network(network), false)
+ SdkBuilder::new_mock().with_network(network)
} else {
// Parse the address list
let address_list = match AddressList::from_str(addresses_str) {
@@ -167,7 +171,7 @@ pub unsafe extern "C" fn dash_sdk_create(config: *const DashSDKConfig) -> DashSD
}
};
- (SdkBuilder::new(address_list).with_network(network), true)
+ SdkBuilder::new(address_list).with_network(network)
}
};
@@ -181,9 +185,7 @@ pub unsafe extern "C" fn dash_sdk_create(config: *const DashSDKConfig) -> DashSD
match sdk_result {
Ok(sdk) => {
- if is_real {
- best_effort_refresh(&sdk, &runtime);
- }
+ best_effort_refresh(&sdk, &runtime);
// Clone Arc into the wrapper
let wrapper = Box::new(SDKWrapper {
sdk,
@@ -229,9 +231,9 @@ pub unsafe extern "C" fn dash_sdk_create_extended(
};
// Parse DAPI addresses
- let (mut builder, is_real) = if base_config.dapi_addresses.is_null() {
+ let mut builder = if base_config.dapi_addresses.is_null() {
// Use mock SDK if no addresses provided
- (SdkBuilder::new_mock().with_network(network), false)
+ SdkBuilder::new_mock().with_network(network)
} else {
let addresses_str = match unsafe { CStr::from_ptr(base_config.dapi_addresses) }.to_str() {
Ok(s) => s,
@@ -245,7 +247,7 @@ pub unsafe extern "C" fn dash_sdk_create_extended(
if addresses_str.is_empty() {
// Use mock SDK if addresses string is empty
- (SdkBuilder::new_mock().with_network(network), false)
+ SdkBuilder::new_mock().with_network(network)
} else {
// Parse the address list
let address_list = match AddressList::from_str(addresses_str) {
@@ -258,7 +260,7 @@ pub unsafe extern "C" fn dash_sdk_create_extended(
}
};
- (SdkBuilder::new(address_list).with_network(network), true)
+ SdkBuilder::new(address_list).with_network(network)
}
};
@@ -297,9 +299,7 @@ pub unsafe extern "C" fn dash_sdk_create_extended(
match sdk_result {
Ok(sdk) => {
- if is_real {
- best_effort_refresh(&sdk, &runtime);
- }
+ best_effort_refresh(&sdk, &runtime);
let wrapper = Box::new(SDKWrapper {
sdk,
runtime,
diff --git a/packages/rs-sdk/src/sdk.rs b/packages/rs-sdk/src/sdk.rs
index c24c0379e99..335553a3092 100644
--- a/packages/rs-sdk/src/sdk.rs
+++ b/packages/rs-sdk/src/sdk.rs
@@ -570,6 +570,22 @@ impl Sdk {
self.protocol_version.load(Ordering::Relaxed)
}
+ /// Returns `true` if this SDK is backed by a mock instead of a live network.
+ ///
+ /// Real (network-backed) SDKs return `false`. Used by callers (e.g. the FFI
+ /// init wiring) to skip the best-effort protocol-version refresh, which would
+ /// otherwise issue a guaranteed-failing proven query against a mock.
+ pub fn is_mock(&self) -> bool {
+ #[cfg(feature = "mocks")]
+ {
+ matches!(self.inner, SdkInstance::Mock { .. })
+ }
+ #[cfg(not(feature = "mocks"))]
+ {
+ false
+ }
+ }
+
// TODO: Move to settings
/// Indicate if the sdk should request and verify proofs.
pub fn prove(&self) -> bool {