Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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.
}
Comment thread
lklimek marked this conversation as resolved.
}

/// 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