Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f4703d6
feat(swift-sdk): use SPV-synced quorums for Platform proof verification
QuantumExplorer Mar 31, 2026
ad6904a
refactor(rs-sdk-ffi): move SPV context provider from Swift to Rust
QuantumExplorer Mar 31, 2026
d9e478e
feat(platform-wallet): add pure-Rust SpvContextProvider
QuantumExplorer Mar 31, 2026
8d2bb27
fix: address CodeRabbit review feedback on SPV quorums PR
QuantumExplorer Apr 2, 2026
fa7e531
feat: use pure-Rust SpvContextProvider, bump rust-dashcore
QuantumExplorer Apr 2, 2026
54b6ed0
fix: eliminate race between didSet and handleNetworkSwitch
QuantumExplorer Apr 2, 2026
cfde224
fix: use try_read() instead of blocking_read(), add dep: prefix
QuantumExplorer Apr 2, 2026
6f67870
Merge v4.1-dev into feat/ios-spv-quorums; re-integrate SPV context pr…
shumkov Jul 11, 2026
05fd41d
style(platform-wallet-ffi): apply rustfmt; drop internal re-integrati…
shumkov Jul 11, 2026
3fad6f8
feat(swift-sdk): verify proofs against SPV quorums via attach-after-b…
shumkov Jul 13, 2026
600b440
feat(sdk): share context provider across SDK clones; live Auto/SPV/Tr…
shumkov Jul 13, 2026
9a2289a
test(sdk,rs-sdk-ffi): cover context-provider clone-sharing + install/…
shumkov Jul 13, 2026
584e68c
fix(rs-platform-wallet): reverse quorum hash to internal order for SP…
shumkov Jul 14, 2026
1deb36b
fix(rs-platform-wallet): guard byte-order via production helper + pin…
shumkov Jul 14, 2026
8263be0
fix(rs-sdk-ffi): install SPV quorum provider as a composite over trusted
shumkov Jul 14, 2026
a53b983
fix(swift-sdk): force SPV install in SPV mode instead of gating on ru…
shumkov Jul 14, 2026
a0fa683
fix(rs-platform-wallet): avoid block_in_place panic on a current-thre…
shumkov Jul 15, 2026
6fc782f
fix(rs-platform-wallet): fail closed on current-thread runtime, not a…
shumkov Jul 15, 2026
3a5e96e
refactor(sdk): add adaptive SPV context provider
shumkov Jul 15, 2026
ba2fbef
docs: remove adaptive SPV provider spec
shumkov Jul 15, 2026
d4046ca
refactor(sdk): separate adaptive and SPV providers
shumkov Jul 16, 2026
4961a0e
chore(sdk): remove unused HTTP dependency
shumkov Jul 16, 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
27 changes: 13 additions & 14 deletions Cargo.lock

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

Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,9 @@ async fn main() -> Result<()> {
// Build SDK with mock core (context provider will handle everything)
let sdk = SdkBuilder::new(AddressList::from_iter(addresses))
.with_core("127.0.0.1", 1, "mock", "mock") // Mock values, won't be used
.with_context_provider(context_provider)
.build()?;

// Set our trusted context provider
sdk.set_context_provider(context_provider);

// Fetch the identity
println!("Fetching identity with proof verification...");
match Identity::fetch(&sdk, identity_id).await {
Expand Down
2 changes: 1 addition & 1 deletion packages/rs-platform-wallet-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ description = "C FFI bindings for platform-wallet"
crate-type = ["staticlib", "cdylib", "rlib"]

[dependencies]
platform-wallet = { path = "../rs-platform-wallet" }
platform-wallet = { path = "../rs-platform-wallet", features = ["spv-context"] }
dpp = { path = "../rs-dpp" }
dash-sdk = { path = "../rs-sdk", features = ["wallet"] }
# Needed for `SignerHandle` + `VTableSigner` so the `*_with_signer`
Expand Down
61 changes: 58 additions & 3 deletions packages/rs-platform-wallet-ffi/src/handle.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use once_cell::sync::Lazy;
use parking_lot::RwLock;
use std::collections::HashMap;
use std::ops::Deref;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;

/// Handle type for FFI objects
pub type Handle = u64;
Expand Down Expand Up @@ -120,13 +122,66 @@ pub static PLATFORM_ADDRESS_WALLET_STORAGE: Lazy<
HandleStorage<platform_wallet::wallet::platform_addresses::PlatformAddressWallet>,
> = Lazy::new(HandleStorage::new);

/// FFI-owned manager plus the optional lease that connects its running SPV
/// runtime to an SDK's fixed SPV-capable context provider.
pub struct PlatformWalletManagerHandle {
manager: platform_wallet::PlatformWalletManager<crate::persistence::FFIPersister>,
spv_source: Option<Arc<platform_wallet::spv_context_provider::SpvQuorumSource>>,
spv_source_controller: Option<Arc<rs_sdk_ffi::SpvSourceController>>,
spv_source_lease: parking_lot::Mutex<Option<rs_sdk_ffi::SpvSourceLease>>,
}

impl PlatformWalletManagerHandle {
pub(crate) fn new(
manager: platform_wallet::PlatformWalletManager<crate::persistence::FFIPersister>,
spv_source: Option<Arc<platform_wallet::spv_context_provider::SpvQuorumSource>>,
spv_source_controller: Option<Arc<rs_sdk_ffi::SpvSourceController>>,
) -> Self {
Self {
manager,
spv_source,
spv_source_controller,
spv_source_lease: parking_lot::Mutex::new(None),
}
}

pub(crate) fn acquire_spv_source(&self) -> Result<(), String> {
let Some(controller) = &self.spv_source_controller else {
return Ok(());
};
let source = self
.spv_source
.as_ref()
.ok_or_else(|| "SPV source controller has no quorum source".to_string())?;
let readiness_source = Arc::clone(source);
let quorum_source: Arc<dyn dash_sdk::platform::ContextProvider> =
Arc::clone(source) as Arc<dyn dash_sdk::platform::ContextProvider>;
let lease = controller
.acquire(quorum_source, Arc::new(move || readiness_source.is_ready()))
.map_err(str::to_string)?;
*self.spv_source_lease.lock() = Some(lease);
Ok(())
}

pub(crate) fn release_spv_source(&self) {
let _ = self.spv_source_lease.lock().take();
}
}

impl Deref for PlatformWalletManagerHandle {
type Target = platform_wallet::PlatformWalletManager<crate::persistence::FFIPersister>;

fn deref(&self) -> &Self::Target {
&self.manager
}
}

/// Storage for PlatformWalletManager handles.
///
/// The manager is generic over the persister type; the FFI binds it
/// to the callback-based [`FFIPersister`](crate::persistence::FFIPersister).
pub static PLATFORM_WALLET_MANAGER_STORAGE: Lazy<
HandleStorage<platform_wallet::PlatformWalletManager<crate::persistence::FFIPersister>>,
> = Lazy::new(HandleStorage::new);
pub static PLATFORM_WALLET_MANAGER_STORAGE: Lazy<HandleStorage<PlatformWalletManagerHandle>> =
Lazy::new(HandleStorage::new);

/// Storage for PlatformWallet handles
pub static PLATFORM_WALLET_STORAGE: Lazy<
Expand Down
77 changes: 67 additions & 10 deletions packages/rs-platform-wallet-ffi/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,70 @@ pub unsafe extern "C" fn platform_wallet_manager_create(
let _runtime_guard = runtime().enter();

let manager = PlatformWalletManager::new(sdk, persister, handler);
let handle = PLATFORM_WALLET_MANAGER_STORAGE.insert(manager);
let handle = PLATFORM_WALLET_MANAGER_STORAGE
.insert(PlatformWalletManagerHandle::new(manager, None, None));
*out_handle = handle;

PlatformWalletFFIResult::ok()
}

/// Create a manager from an owning SDK handle and prepare an SPV source for
/// that SDK's fixed SPV-capable provider.
///
/// The SDK handle is borrowed only for this synchronous call. The manager
/// retains an `Sdk` clone, never the raw handle.
#[no_mangle]
pub unsafe extern "C" fn platform_wallet_manager_create_with_sdk(
sdk_handle: *mut rs_sdk_ffi::SDKHandle,
persistence: *const PersistenceCallbacks,
event_handler: *const EventHandlerCallbacks,
out_handle: *mut Handle,
) -> PlatformWalletFFIResult {
check_ptr!(sdk_handle);
check_ptr!(persistence);
check_ptr!(event_handler);
check_ptr!(out_handle);

let sdk_ptr = rs_sdk_ffi::dash_sdk_get_inner_sdk_ptr(sdk_handle);
if sdk_ptr.is_null() {
return PlatformWalletFFIResult::err(
PlatformWalletFFIResultCode::ErrorInvalidParameter,
"SDK has no inner SDK".to_string(),
);
}

let sdk = Arc::new((*(sdk_ptr as *const Sdk)).clone());
let persister = Arc::new(FFIPersister::new(std::ptr::read(persistence)));
let handler: Arc<dyn platform_wallet::PlatformEventHandler> =
Arc::new(FFIEventHandler::new(std::ptr::read(event_handler)));

let _runtime_guard = runtime().enter();
let manager = PlatformWalletManager::new(Arc::clone(&sdk), persister, handler);
let spv = manager.spv_arc();
let provider = Arc::new(platform_wallet::spv_context_provider::SpvQuorumSource::new(
Arc::clone(&spv),
sdk.network,
));
let controller = match rs_sdk_ffi::dash_sdk_spv_source_controller(sdk_handle) {
Ok(controller) => controller,
Err(message) => {
drop(_runtime_guard);
runtime().block_on(manager.shutdown());
return PlatformWalletFFIResult::err(
PlatformWalletFFIResultCode::ErrorInvalidParameter,
message,
);
}
};

*out_handle = PLATFORM_WALLET_MANAGER_STORAGE.insert(PlatformWalletManagerHandle::new(
manager,
controller.as_ref().map(|_| provider),
controller,
));
PlatformWalletFFIResult::ok()
}

/// Map the C `has_x: bool` + `x` companion-pair idiom to a Rust `Option<u32>`.
///
/// `has == true` yields `Some(value)` — including `Some(0)`, kept distinct
Expand Down Expand Up @@ -354,15 +412,14 @@ pub unsafe extern "C" fn platform_wallet_manager_destroy(
handle: Handle,
) -> PlatformWalletFFIResult {
if let Some(manager) = PLATFORM_WALLET_MANAGER_STORAGE.remove(handle) {
// Run the full lifecycle shutdown to completion, not just the
// platform-address sync. Every background task (identity sync,
// shielded sync, the wallet-event adapter) can fire callbacks
// through the host-owned `context` pointer; once `destroy`
// returns the host may free that context, so no task may be
// left alive to fire a callback against freed memory.
// `shutdown()` is idempotent, so this is safe even if the host
// already stopped some sync managers before calling destroy.
runtime().block_on(manager.shutdown());
// Stop SPV first because its event manager retains host callback
// pointers. Then detach this manager's matching source lease before
// quiescing every remaining callback-capable task.
runtime().block_on(async {
let _ = manager.spv().stop().await;
manager.release_spv_source();
manager.shutdown().await;
});
}
PlatformWalletFFIResult::ok()
}
Expand Down
7 changes: 7 additions & 0 deletions packages/rs-platform-wallet-ffi/src/spv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::sync::Arc;

use dashcore::sml::llmq_type::LlmqDevnetParams;
use platform_wallet::spv::{
Expand Down Expand Up @@ -457,6 +458,11 @@ pub unsafe extern "C" fn platform_wallet_manager_spv_start(
};

if start_result.is_ok() {
if let Err(message) = manager.acquire_spv_source() {
let spv_to_stop = Arc::clone(&spv);
let _ = block_on_worker(async move { spv_to_stop.stop().await });
return Err(platform_wallet::PlatformWalletError::SpvError(message));
}
let _guard = runtime().enter();
spv.spawn_run_loop();
}
Expand All @@ -479,6 +485,7 @@ pub unsafe extern "C" fn platform_wallet_manager_spv_stop(
runtime().block_on(async {
let _ = manager.spv().stop().await;
});
manager.release_spv_source();
});
unwrap_option_or_return!(option);
PlatformWalletFFIResult::ok()
Expand Down
10 changes: 10 additions & 0 deletions packages/rs-platform-wallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ dash-spv = { workspace = true }
# Core dependencies
dashcore = { workspace = true }

# SPV context provider dependency (optional). `dash-spv` and `tokio` are
# already non-optional dependencies above, so the `spv-context` feature only
# needs to pull in the context-provider trait crate.
dash-context-provider = { path = "../rs-context-provider", optional = true }

# Standard dependencies
thiserror = "1.0"
async-trait = "0.1"
Expand Down Expand Up @@ -112,6 +117,11 @@ rs-sdk-trusted-context-provider = { path = "../rs-sdk-trusted-context-provider"

[features]
default = ["bls", "eddsa"]
# SPV-backed Platform context provider (delegates quorum lookups to the SPV
# runtime). `dash-spv`/`tokio` are already non-optional deps; this pulls in the
# context-provider trait crate plus `rt-multi-thread` (the sync->async bridge
# in `spv_context_provider` uses `tokio::task::block_in_place`).
spv-context = ["dep:dash-context-provider", "tokio/rt-multi-thread"]
bls = ["key-wallet/bls", "key-wallet-manager/bls"]
eddsa = ["key-wallet/eddsa", "key-wallet-manager/eddsa"]
shielded = ["dep:grovedb-commitment-tree", "dep:rusqlite", "dep:zip32", "dep:futures", "dash-sdk/shielded", "dpp/shielded-client"]
Expand Down
3 changes: 3 additions & 0 deletions packages/rs-platform-wallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ pub(crate) mod test_support;
mod util;
pub mod wallet;

#[cfg(feature = "spv-context")]
pub mod spv_context_provider;

pub use error::PlatformWalletError;
pub use events::{PlatformEventHandler, PlatformEventManager};
pub use key_wallet::wallet::managed_wallet_info::asset_lock_builder::AssetLockFundingType;
Expand Down
Loading
Loading