Skip to content
9 changes: 9 additions & 0 deletions packages/rs-dapi-client/src/dapi_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
69 changes: 68 additions & 1 deletion packages/rs-platform-wallet-ffi/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Comment thread
lklimek marked this conversation as resolved.

NotFound = 98, // Used exclusively for all the Option that are retuned as errors
ErrorUnknown = 99,
Expand Down Expand Up @@ -156,7 +168,20 @@ impl<T> From<Option<T>> for PlatformWalletFFIResult {

impl From<PlatformWalletError> 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())
}
}

Expand Down Expand Up @@ -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}"
);
}
Comment thread
lklimek marked this conversation as resolved.

/// 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);
Comment thread
lklimek marked this conversation as resolved.
}
}
38 changes: 38 additions & 0 deletions packages/rs-platform-wallet/src/error.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<PlatformAddress>,
/// 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),

Expand Down
22 changes: 22 additions & 0 deletions packages/rs-platform-wallet/src/spv/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Comment thread
lklimek marked this conversation as resolved.
Outdated
}
Comment thread
lklimek marked this conversation as resolved.
Outdated
}

/// Stop SPV sync gracefully.
///
/// If a `run()` task was spawned via [`spawn_in_background`], its
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,20 @@ impl IdentityManager {
.sum::<usize>()
}

/// 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<Identifier> {
let mut out: Vec<Identifier> = 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()
Expand Down
Loading