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 {