Skip to content
Draft
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,

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}"
);
}

/// 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);
}
}
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
33 changes: 33 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();
}
}

/// Stop SPV sync gracefully.
///
/// If a `run()` task was spawned via [`spawn_in_background`], its
Expand Down Expand Up @@ -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<PlatformEventManager> {
&self.event_manager
}

/// Read the unix-seconds block time of the SPV header storage's
/// current tip.
///
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