Skip to content
Merged
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
86 changes: 85 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,19 @@ pub enum PlatformWalletFFIResultCode {
ErrorInvalidIdentifier = 10,
ErrorMemoryAllocation = 11,
ErrorUtf8Conversion = 12,
/// 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),
/// `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 +169,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 +402,62 @@ 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 dpp::address_funds::PlatformAddress;
use key_wallet::account::StandardAccountType;

let cases: Vec<PlatformWalletError> = vec![
PlatformWalletError::NoSpendableInputs {
account_type: StandardAccountType::BIP44Account,
account_index: 0,
context: "wallet empty in test".to_string(),
},
PlatformWalletError::OnlyOutputAddressesFunded {
funded_outputs: Vec::<PlatformAddress>::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"
);
}
}
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.
}
}
11 changes: 2 additions & 9 deletions packages/rs-platform-wallet-ffi/src/identity_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<_>>()
});
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()
Expand Down
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
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
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Expand All @@ -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
}
Expand All @@ -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)
}
Expand Down
Loading