From 62e03c62988c26996ff5da3ff9b99495e61e8586 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Fri, 15 May 2026 14:36:03 +0200 Subject: [PATCH 1/8] feat(rs-platform-wallet): SPV runtime accessors + FFI error mapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Carve-out of clusters D, E, K from #3549 (e2e framework PR) as an independent production change on v3.1-dev. D — SPV runtime accessors: SpvRuntime::cancel_background() (sync cancellation-token fire for panic-hook data-dir-lock release) and SpvRuntime::event_manager(); IdentityManager::identity_ids() snapshot. E — FFI error mapping: dedicated ErrorNoSelectableInputs=14 (and reserved ErrorArithmeticOverflow=13); NoSpendableInputs / OnlyOutputAddressesFunded / OnlyDustInputs now map to the dedicated FFI code instead of flattening to ErrorUnknown. K — DAPI dispatch trace log: one tracing::trace! emitting the resolved endpoint/method. Pure observability. E's match arm consumes three PlatformWalletError variants that live in the still-open #3554 (OnlyOutputAddressesFunded, OnlyDustInputs) and #3585 (NoSpendableInputs). Per maintainer decision this PR is independent on v3.1-dev and copies those variant definitions verbatim so it compiles standalone; duplicate definitions are expected and resolved at merge. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/rs-dapi-client/src/dapi_client.rs | 9 +++ packages/rs-platform-wallet-ffi/src/error.rs | 69 ++++++++++++++++++- packages/rs-platform-wallet/src/error.rs | 38 ++++++++++ .../rs-platform-wallet/src/spv/runtime.rs | 33 +++++++++ .../identity/state/manager/accessors.rs | 14 ++++ 5 files changed, 162 insertions(+), 1 deletion(-) diff --git a/packages/rs-dapi-client/src/dapi_client.rs b/packages/rs-dapi-client/src/dapi_client.rs index 1b9f07558f4..5c20d46dc01 100644 --- a/packages/rs-dapi-client/src/dapi_client.rs +++ b/packages/rs-dapi-client/src/dapi_client.rs @@ -575,6 +575,15 @@ impl DapiRequestExecutor for DapiClient { }); }; + // Rec 3 — explicit trace event so the resolved DAPI endpoint + // appears in flat plain-text log output (not just the span context). + tracing::trace!( + target: "dapi_client::dispatch", + ?address, + method = request.method_name(), + request_type = request.request_name(), + "dispatching request to DAPI endpoint" + ); tracing::trace!( ?request, "calling {} with {} request", diff --git a/packages/rs-platform-wallet-ffi/src/error.rs b/packages/rs-platform-wallet-ffi/src/error.rs index adbe771c8c0..e74449a3ffa 100644 --- a/packages/rs-platform-wallet-ffi/src/error.rs +++ b/packages/rs-platform-wallet-ffi/src/error.rs @@ -76,6 +76,18 @@ pub enum PlatformWalletFFIResultCode { ErrorInvalidIdentifier = 10, ErrorMemoryAllocation = 11, ErrorUtf8Conversion = 12, + /// Reserved code — currently unused. Kept to preserve numeric ABI for + /// downstream consumers that compiled against this enum. + ErrorArithmeticOverflow = 13, + /// Auto-select had no candidate inputs. Covers all three "can't-select-inputs" + /// wallet variants: `NoSpendableInputs` (account has nothing spendable), + /// `OnlyOutputAddressesFunded` (every funded address is also a destination), + /// and `OnlyDustInputs` (every funded address is below `min_input_amount`). + /// The typed Display rendering survives via the result message so callers + /// can distinguish the underlying cause. Caller must rotate to a fresh + /// receive address, consolidate sub-min balances, or fall back to + /// `InputSelection::Explicit`. + ErrorNoSelectableInputs = 14, NotFound = 98, // Used exclusively for all the Option that are retuned as errors ErrorUnknown = 99, @@ -156,7 +168,20 @@ impl From> for PlatformWalletFFIResult { impl From for PlatformWalletFFIResult { fn from(error: PlatformWalletError) -> Self { - PlatformWalletFFIResult::err(PlatformWalletFFIResultCode::ErrorUnknown, error.to_string()) + // Map the typed wallet error variants explicitly so they + // don't flatten to ErrorUnknown at the FFI boundary. The + // catch-all ErrorUnknown remains for variants the FFI hasn't + // assigned a dedicated code yet — those still carry the + // typed Display rendering as the message. + let code = match &error { + PlatformWalletError::NoSpendableInputs { .. } + | PlatformWalletError::OnlyOutputAddressesFunded { .. } + | PlatformWalletError::OnlyDustInputs { .. } => { + PlatformWalletFFIResultCode::ErrorNoSelectableInputs + } + _ => PlatformWalletFFIResultCode::ErrorUnknown, + }; + PlatformWalletFFIResult::err(code, error.to_string()) } } @@ -376,4 +401,46 @@ mod tests { ); assert!(!r.message.is_null()); } + + /// The three "can't-select-inputs" wallet variants (`NoSpendableInputs`, + /// `OnlyOutputAddressesFunded`, `OnlyDustInputs`) all map to the dedicated + /// `ErrorNoSelectableInputs` FFI code rather than flattening to + /// `ErrorUnknown`, and the typed Display rendering survives across the + /// boundary so callers can distinguish the underlying cause from the + /// message string. + #[test] + fn no_selectable_inputs_maps_to_dedicated_code() { + use key_wallet::account::StandardAccountType; + let err = PlatformWalletError::NoSpendableInputs { + account_type: StandardAccountType::BIP44Account, + account_index: 0, + context: "wallet empty in test".to_string(), + }; + let rendered = err.to_string(); + let result: PlatformWalletFFIResult = err.into(); + assert_eq!( + result.code, + PlatformWalletFFIResultCode::ErrorNoSelectableInputs + ); + assert!(!result.message.is_null()); + let msg = unsafe { std::ffi::CStr::from_ptr(result.message) } + .to_string_lossy() + .into_owned(); + assert_eq!(msg, rendered); + assert!( + msg.contains("no spendable inputs"), + "Display payload must survive: {msg}" + ); + } + + /// Other wallet-error variants without a dedicated FFI arm still + /// fall through to `ErrorUnknown` while carrying the typed + /// Display rendering as the message. Pin this so the catch-all + /// stays the only `ErrorUnknown` source. + #[test] + fn unmapped_variants_fall_through_to_unknown() { + let err = PlatformWalletError::AddressOperation("explicit fallthrough".to_string()); + let result: PlatformWalletFFIResult = err.into(); + assert_eq!(result.code, PlatformWalletFFIResultCode::ErrorUnknown); + } } diff --git a/packages/rs-platform-wallet/src/error.rs b/packages/rs-platform-wallet/src/error.rs index 006e9b01331..a7ac761ab93 100644 --- a/packages/rs-platform-wallet/src/error.rs +++ b/packages/rs-platform-wallet/src/error.rs @@ -1,4 +1,7 @@ +use dpp::address_funds::PlatformAddress; +use dpp::fee::Credits; use dpp::identifier::Identifier; +use key_wallet::account::StandardAccountType; use key_wallet::Network; /// Errors that can occur in platform wallet operations @@ -60,6 +63,41 @@ pub enum PlatformWalletError { #[error("Transaction building failed: {0}")] TransactionBuild(String), + #[error("no spendable inputs available on {account_type} account {account_index}: {context}")] + NoSpendableInputs { + account_type: StandardAccountType, + account_index: u32, + context: String, + }, + + #[error( + "no selectable inputs: only funded addresses appear as destinations \ + (funded_outputs={funded_outputs:?}, min_input_amount={min_input_amount}); \ + rotate to a fresh receive address, consolidate funds, or use \ + InputSelection::Explicit" + )] + OnlyOutputAddressesFunded { + /// Funded addresses dropped by the input-equals-output filter. + funded_outputs: Vec, + /// Per-input minimum from the active platform version. + min_input_amount: Credits, + }, + + #[error( + "no selectable inputs: every funded address is below the per-input \ + minimum (sub_min_count={sub_min_count}, sub_min_aggregate={sub_min_aggregate} \ + credits, min_input_amount={min_input_amount}); consolidate funds or use \ + InputSelection::Explicit" + )] + OnlyDustInputs { + /// Number of addresses with a positive balance below `min_input_amount`. + sub_min_count: usize, + /// Aggregate of those sub-minimum balances. + sub_min_aggregate: Credits, + /// Per-input minimum from the active platform version. + min_input_amount: Credits, + }, + #[error("Asset lock proof waiting failed: {0}")] AssetLockProofWait(String), diff --git a/packages/rs-platform-wallet/src/spv/runtime.rs b/packages/rs-platform-wallet/src/spv/runtime.rs index d0c56e48b7a..c563c3da824 100644 --- a/packages/rs-platform-wallet/src/spv/runtime.rs +++ b/packages/rs-platform-wallet/src/spv/runtime.rs @@ -179,6 +179,28 @@ impl SpvRuntime { result } + /// Synchronously fire the background `run()` task's cancellation + /// token, if any. The actual storage/lockfile teardown still + /// happens asynchronously inside the spawned task as it unwinds + /// to its `self.stop().await` epilogue — this method just wakes + /// it. Idempotent: subsequent calls (and a follow-up [`stop`]) + /// see `None` and return immediately. + /// + /// Designed for sync contexts where awaiting [`stop`] isn't + /// possible — for example a `std::panic::set_hook` callback that + /// needs to release the dash-spv data-dir lock before the next + /// init attempt without blocking the panicking thread. + pub fn cancel_background(&self) { + if let Some(token) = self + .background_cancel + .lock() + .expect("background_cancel poisoned") + .take() + { + token.cancel(); + } + } + /// Stop SPV sync gracefully. /// /// If a `run()` task was spawned via [`spawn_in_background`], its @@ -233,6 +255,17 @@ impl SpvRuntime { Some(client.sync_progress().await) } + /// The [`PlatformEventManager`] this runtime dispatches SPV events + /// through. Exposed so consumers (e.g. the e2e framework) can + /// register additional [`crate::events::PlatformEventHandler`]s + /// after construction — for example, to observe + /// `SyncEvent::ManagerError` while waiting for mn-list sync so + /// hard-stalls surface immediately instead of burning the full + /// timeout. + pub fn event_manager(&self) -> &Arc { + &self.event_manager + } + /// Read the unix-seconds block time of the SPV header storage's /// current tip. /// diff --git a/packages/rs-platform-wallet/src/wallet/identity/state/manager/accessors.rs b/packages/rs-platform-wallet/src/wallet/identity/state/manager/accessors.rs index cfe81e52560..4e430588bb2 100644 --- a/packages/rs-platform-wallet/src/wallet/identity/state/manager/accessors.rs +++ b/packages/rs-platform-wallet/src/wallet/identity/state/manager/accessors.rs @@ -104,6 +104,20 @@ impl IdentityManager { .sum::() } + /// Snapshot of every managed identity's `Identifier` across both + /// buckets. Order is unspecified — callers that need a stable + /// order should sort the returned `Vec`. + pub fn identity_ids(&self) -> Vec { + let mut out: Vec = Vec::with_capacity(self.identity_count()); + out.extend(self.out_of_wallet_identities.keys().copied()); + for inner in self.wallet_identities.values() { + for managed in inner.values() { + out.push(managed.identity.id()); + } + } + out + } + /// `true` iff both buckets are empty. pub fn is_empty(&self) -> bool { self.out_of_wallet_identities.is_empty() && self.wallet_identities.is_empty() From 66a1a64abd500691d97b49a5197b9eccd41acc0c Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 18 May 2026 10:51:14 +0200 Subject: [PATCH 2/8] refactor(platform-wallet): drop SpvRuntime::event_manager() accessor (superseded by #3549) Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/rs-platform-wallet/src/spv/runtime.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/rs-platform-wallet/src/spv/runtime.rs b/packages/rs-platform-wallet/src/spv/runtime.rs index c563c3da824..813d3f62e78 100644 --- a/packages/rs-platform-wallet/src/spv/runtime.rs +++ b/packages/rs-platform-wallet/src/spv/runtime.rs @@ -255,17 +255,6 @@ impl SpvRuntime { Some(client.sync_progress().await) } - /// The [`PlatformEventManager`] this runtime dispatches SPV events - /// through. Exposed so consumers (e.g. the e2e framework) can - /// register additional [`crate::events::PlatformEventHandler`]s - /// after construction — for example, to observe - /// `SyncEvent::ManagerError` while waiting for mn-list sync so - /// hard-stalls surface immediately instead of burning the full - /// timeout. - pub fn event_manager(&self) -> &Arc { - &self.event_manager - } - /// Read the unix-seconds block time of the SPV header storage's /// current tip. /// From 00455a1e1d812682dea22a0b3ae6db1ed3b42a54 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 21 May 2026 13:51:15 +0200 Subject: [PATCH 3/8] fix(platform-wallet): cancel_background() best-effort + no .expect on poisoned lock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SpvRuntime::cancel_background() is intentionally invoked from sync contexts — notably std::panic::set_hook callbacks where the background_cancel mutex may already be poisoned by the panicking thread. Replace the .expect("background_cancel poisoned") with .unwrap_or_else(|p| p.into_inner()) so the cancel-token fire remains best-effort instead of escalating a first panic into a second. Doc comment now states honestly that this method only wakes the spawned task — data-dir lock teardown is asynchronous inside the task's stop().await epilogue and is NOT guaranteed by the time cancel_background() returns. Callers that need that guarantee must await stop() from an async context. The two other .expect() sites in stop() and spawn_in_background() are not on the panic-recovery path and stay unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../rs-platform-wallet/src/spv/runtime.rs | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/rs-platform-wallet/src/spv/runtime.rs b/packages/rs-platform-wallet/src/spv/runtime.rs index 813d3f62e78..7e75d696143 100644 --- a/packages/rs-platform-wallet/src/spv/runtime.rs +++ b/packages/rs-platform-wallet/src/spv/runtime.rs @@ -179,22 +179,31 @@ impl SpvRuntime { result } - /// Synchronously fire the background `run()` task's cancellation - /// token, if any. The actual storage/lockfile teardown still - /// happens asynchronously inside the spawned task as it unwinds - /// to its `self.stop().await` epilogue — this method just wakes - /// it. Idempotent: subsequent calls (and a follow-up [`stop`]) + /// Best-effort: fire the background `run()` task's cancel token if one + /// is registered. Teardown of the dash-spv client and its data-dir + /// lockfile still happens asynchronously inside the spawned task as it + /// unwinds to its `self.stop().await` epilogue — this method only wakes + /// the task. Idempotent: subsequent calls (and a follow-up [`stop`]) /// see `None` and return immediately. /// - /// Designed for sync contexts where awaiting [`stop`] isn't - /// possible — for example a `std::panic::set_hook` callback that - /// needs to release the dash-spv data-dir lock before the next - /// init attempt without blocking the panicking thread. + /// Designed for sync contexts where awaiting [`stop`] isn't possible — + /// for example a `std::panic::set_hook` callback that wants to nudge the + /// SPV task toward shutdown without blocking the panicking thread. + /// + /// This method does **not** guarantee the dash-spv data-dir lock has + /// been released by the time it returns. Callers that need that + /// guarantee (e.g. before reinitializing on the same data directory) + /// must `await stop()` from an async context instead. + /// + /// Tolerates a poisoned `background_cancel` mutex — the panic-hook use + /// case is precisely when the lock may already be poisoned, so the + /// guard is recovered via `PoisonError::into_inner` rather than + /// panicking again. pub fn cancel_background(&self) { if let Some(token) = self .background_cancel .lock() - .expect("background_cancel poisoned") + .unwrap_or_else(|p| p.into_inner()) .take() { token.cancel(); From 00dcaa20776700b419a58fcc5b51008fe8789505 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 21 May 2026 13:51:24 +0200 Subject: [PATCH 4/8] test(platform-wallet-ffi): pin all three variants to ErrorNoSelectableInputs Extend no_selectable_inputs_maps_to_dedicated_code to a table-driven test that exercises every PlatformWalletError variant funneled into ErrorNoSelectableInputs by the From impl: NoSpendableInputs, OnlyOutputAddressesFunded, and OnlyDustInputs. Each case asserts the FFI result.code is ErrorNoSelectableInputs and that result.message (read via CStr::from_ptr) equals the original err.to_string() Display rendering verbatim, so downstream callers can still distinguish the underlying cause from the message text even though all three share one FFI code. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/rs-platform-wallet-ffi/src/error.rs | 56 +++++++++++++------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/packages/rs-platform-wallet-ffi/src/error.rs b/packages/rs-platform-wallet-ffi/src/error.rs index e74449a3ffa..5094271ab7f 100644 --- a/packages/rs-platform-wallet-ffi/src/error.rs +++ b/packages/rs-platform-wallet-ffi/src/error.rs @@ -410,27 +410,43 @@ mod tests { /// message string. #[test] fn no_selectable_inputs_maps_to_dedicated_code() { + use dpp::address_funds::PlatformAddress; use key_wallet::account::StandardAccountType; - let err = PlatformWalletError::NoSpendableInputs { - account_type: StandardAccountType::BIP44Account, - account_index: 0, - context: "wallet empty in test".to_string(), - }; - let rendered = err.to_string(); - let result: PlatformWalletFFIResult = err.into(); - assert_eq!( - result.code, - PlatformWalletFFIResultCode::ErrorNoSelectableInputs - ); - assert!(!result.message.is_null()); - let msg = unsafe { std::ffi::CStr::from_ptr(result.message) } - .to_string_lossy() - .into_owned(); - assert_eq!(msg, rendered); - assert!( - msg.contains("no spendable inputs"), - "Display payload must survive: {msg}" - ); + + let cases: Vec = vec![ + PlatformWalletError::NoSpendableInputs { + account_type: StandardAccountType::BIP44Account, + account_index: 0, + context: "wallet empty in test".to_string(), + }, + PlatformWalletError::OnlyOutputAddressesFunded { + funded_outputs: Vec::::new(), + min_input_amount: 1_000, + }, + PlatformWalletError::OnlyDustInputs { + sub_min_count: 3, + sub_min_aggregate: 500, + min_input_amount: 1_000, + }, + ]; + + for err in cases { + let rendered = err.to_string(); + let result: PlatformWalletFFIResult = err.into(); + assert_eq!( + result.code, + PlatformWalletFFIResultCode::ErrorNoSelectableInputs, + "variant should map to ErrorNoSelectableInputs (rendered: {rendered})" + ); + assert!(!result.message.is_null()); + let msg = unsafe { std::ffi::CStr::from_ptr(result.message) } + .to_string_lossy() + .into_owned(); + assert_eq!( + msg, rendered, + "Display payload must survive the FFI boundary verbatim" + ); + } } /// Other wallet-error variants without a dedicated FFI arm still From 042372411a5dbcbf479214ed1f88c1d874ff596a Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 21 May 2026 13:51:35 +0200 Subject: [PATCH 5/8] feat(swift-sdk): mirror FFI codes 13/14 in PlatformWalletResult MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Swift mirrors for the two new PlatformWalletFFIResultCode variants exposed by platform-wallet-ffi: - ErrorArithmeticOverflow (13) — reserved, ABI-preserving slot - ErrorNoSelectableInputs (14) — covers NoSpendableInputs / OnlyOutputAddressesFunded / OnlyDustInputs (distinguished by message text) Wire-up: - PlatformWalletResultCode gains .errorArithmeticOverflow / .errorNoSelectableInputs cases at raw values 13 / 14 - init(ffi:) maps the cbindgen-generated PLATFORM_WALLET_FFI_RESULT_CODE_ERROR_* constants - PlatformWalletError gains typed .arithmeticOverflow(String) / .noSelectableInputs(String) cases, with their message strings flowing through errorDescription and init(result:) Swift toolchain not present on this checkout — verification deferred to CI. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../PlatformWallet/PlatformWalletResult.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletResult.swift b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletResult.swift index 199d07bd5e3..1d974b71916 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletResult.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletResult.swift @@ -18,6 +18,8 @@ public enum PlatformWalletResultCode: Int32, Sendable { case errorInvalidIdentifier = 10 case errorMemoryAllocation = 11 case errorUtf8Conversion = 12 + case errorArithmeticOverflow = 13 + case errorNoSelectableInputs = 14 case notFound = 98 case errorUnknown = 99 @@ -49,6 +51,10 @@ public enum PlatformWalletResultCode: Int32, Sendable { self = .errorMemoryAllocation case PLATFORM_WALLET_FFI_RESULT_CODE_ERROR_UTF8_CONVERSION: self = .errorUtf8Conversion + case PLATFORM_WALLET_FFI_RESULT_CODE_ERROR_ARITHMETIC_OVERFLOW: + self = .errorArithmeticOverflow + case PLATFORM_WALLET_FFI_RESULT_CODE_ERROR_NO_SELECTABLE_INPUTS: + self = .errorNoSelectableInputs case PLATFORM_WALLET_FFI_RESULT_CODE_NOT_FOUND: self = .notFound case PLATFORM_WALLET_FFI_RESULT_CODE_ERROR_UNKNOWN: @@ -124,6 +130,8 @@ public enum PlatformWalletError: LocalizedError { case serialization(String) case deserialization(String) case memoryAllocation(String) + case arithmeticOverflow(String) + case noSelectableInputs(String) case notFound(String) case unknown(String) @@ -136,6 +144,7 @@ public enum PlatformWalletError: LocalizedError { .invalidIdentifier(let m), .invalidNetwork(let m), .walletOperation(let m), .identityNotFound(let m), .contactNotFound(let m), .utf8Conversion(let m), .serialization(let m), .deserialization(let m), .memoryAllocation(let m), + .arithmeticOverflow(let m), .noSelectableInputs(let m), .notFound(let m), .unknown(let m): return m } @@ -160,6 +169,8 @@ public enum PlatformWalletError: LocalizedError { case .errorInvalidIdentifier: self = .invalidIdentifier(detail) case .errorMemoryAllocation: self = .memoryAllocation(detail) case .errorUtf8Conversion: self = .utf8Conversion(detail) + case .errorArithmeticOverflow: self = .arithmeticOverflow(detail) + case .errorNoSelectableInputs: self = .noSelectableInputs(detail) case .notFound: self = .notFound(detail) case .errorUnknown: self = .unknown(detail) } From d2526112576e55e553cabe7d0b0fe3e162952702 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 25 May 2026 16:35:08 +0200 Subject: [PATCH 6/8] refactor(platform-wallet): drop SpvRuntime cancellation (moved to #3549; rust-dashcore #772 superseded the inner-token API) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rust-dashcore PR #772 removed the external CancellationToken parameter from DashSpvClient::run() in favour of an internal Arc>. The v3.1-dev pin (rev f569e7b7b9...) carries that change, so the carved-out SpvRuntime::cancel_background() infrastructure on this PR no longer has a producer. Its right home is the #3549 e2e framework branch which retains the design on an older pin. The post-merge state on this PR left a dangling cancel_background() method referencing a background_cancel field that no longer exists. Remove the method outright — the file is now byte-identical to v3.1-dev's runtime.rs. PR #3651 shrinks to its SPV-runtime non-cancel accessors + FFI error mapping for NoSelectableInputs + ErrorArithmeticOverflow reserved slot. Co-Authored-By: Claude Opus 4.6 --- .../rs-platform-wallet/src/spv/runtime.rs | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/packages/rs-platform-wallet/src/spv/runtime.rs b/packages/rs-platform-wallet/src/spv/runtime.rs index 35c5b42b0a4..593a5aabefd 100644 --- a/packages/rs-platform-wallet/src/spv/runtime.rs +++ b/packages/rs-platform-wallet/src/spv/runtime.rs @@ -152,37 +152,6 @@ impl SpvRuntime { result } - /// Best-effort: fire the background `run()` task's cancel token if one - /// is registered. Teardown of the dash-spv client and its data-dir - /// lockfile still happens asynchronously inside the spawned task as it - /// unwinds to its `self.stop().await` epilogue — this method only wakes - /// the task. Idempotent: subsequent calls (and a follow-up [`stop`]) - /// see `None` and return immediately. - /// - /// Designed for sync contexts where awaiting [`stop`] isn't possible — - /// for example a `std::panic::set_hook` callback that wants to nudge the - /// SPV task toward shutdown without blocking the panicking thread. - /// - /// This method does **not** guarantee the dash-spv data-dir lock has - /// been released by the time it returns. Callers that need that - /// guarantee (e.g. before reinitializing on the same data directory) - /// must `await stop()` from an async context instead. - /// - /// Tolerates a poisoned `background_cancel` mutex — the panic-hook use - /// case is precisely when the lock may already be poisoned, so the - /// guard is recovered via `PoisonError::into_inner` rather than - /// panicking again. - pub fn cancel_background(&self) { - if let Some(token) = self - .background_cancel - .lock() - .unwrap_or_else(|p| p.into_inner()) - .take() - { - token.cancel(); - } - } - /// Stop SPV sync gracefully. pub async fn stop(&self) -> Result<(), PlatformWalletError> { let mut client = self.client.write().await; From dd9eb2f2635baacea130057f7953816883104d6d Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 25 May 2026 16:02:35 +0200 Subject: [PATCH 7/8] refactor(rs-platform-wallet-ffi): use IdentityManager::identity_ids() in FFI entrypoint (PR #3651 CMT-001) Route identity_manager_get_all_identity_ids through the dedicated identity_ids() accessor instead of flattening buckets manually via all_identities().map(|i| i.id()). Drops the intermediate Vec<&Identity> allocation and the now-unused IdentityGettersV0 import. Co-Authored-By: Claude Opus 4.6 --- .../rs-platform-wallet-ffi/src/identity_manager.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/rs-platform-wallet-ffi/src/identity_manager.rs b/packages/rs-platform-wallet-ffi/src/identity_manager.rs index 71149bf6b9d..95e472a46f3 100644 --- a/packages/rs-platform-wallet-ffi/src/identity_manager.rs +++ b/packages/rs-platform-wallet-ffi/src/identity_manager.rs @@ -102,17 +102,10 @@ pub unsafe extern "C" fn identity_manager_get_all_identity_ids( manager_handle: Handle, out_array: *mut IdentifierArray, ) -> PlatformWalletFFIResult { - use dpp::identity::accessors::IdentityGettersV0; - check_ptr!(out_array); - let option = IDENTITY_MANAGER_STORAGE.with_item(manager_handle, |manager| { - manager - .all_identities() - .into_iter() - .map(|i| i.id()) - .collect::>() - }); + let option = + IDENTITY_MANAGER_STORAGE.with_item(manager_handle, |manager| manager.identity_ids()); let ids = unwrap_option_or_return!(option); unsafe { *out_array = IdentifierArray::new(ids) }; PlatformWalletFFIResult::ok() From 10ec8ce6fea41ce2bec4c6c7a577590987c3b68f Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 25 May 2026 16:02:55 +0200 Subject: [PATCH 8/8] docs(rs-platform-wallet-ffi): clarify ErrorArithmeticOverflow=13 is a reserved slot for #3549 (PR #3651 CMT-002) Rewrite the doc comment on ErrorArithmeticOverflow. Code 13 did not exist on v3.1-dev prior to this PR (the tail was 12, jumping to 99), so the "preserve numeric ABI" framing was misleading. The slot exists to receive a producer arriving via #3549; reserving it here keeps the Swift/Kotlin mirror enums numerically aligned with the eventual producer. Co-Authored-By: Claude Opus 4.6 --- packages/rs-platform-wallet-ffi/src/error.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/rs-platform-wallet-ffi/src/error.rs b/packages/rs-platform-wallet-ffi/src/error.rs index 5094271ab7f..e5fea7e2efc 100644 --- a/packages/rs-platform-wallet-ffi/src/error.rs +++ b/packages/rs-platform-wallet-ffi/src/error.rs @@ -76,8 +76,9 @@ pub enum PlatformWalletFFIResultCode { ErrorInvalidIdentifier = 10, ErrorMemoryAllocation = 11, ErrorUtf8Conversion = 12, - /// Reserved code — currently unused. Kept to preserve numeric ABI for - /// downstream consumers that compiled against this enum. + /// Reserved slot for the arithmetic-overflow mapping arriving via #3549 — + /// no in-tree producer today. Holding the slot here keeps language-mirror + /// enums (Swift, Kotlin) numerically aligned with the eventual producer. ErrorArithmeticOverflow = 13, /// Auto-select had no candidate inputs. Covers all three "can't-select-inputs" /// wallet variants: `NoSpendableInputs` (account has nothing spendable),