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
10 changes: 5 additions & 5 deletions src/ui/dashpay/contact_requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ use crate::ui::components::wallet_unlock_popup::{
WalletUnlockPopup, WalletUnlockResult, try_open_wallet_no_password, wallet_needs_unlock,
};
use crate::ui::components::{MessageBanner, ResultBannerExt};
use crate::ui::identities::get_selected_wallet;
use crate::ui::identities::keys::add_key_screen::AddKeyScreen;
use crate::ui::identities::{auto_selected_wallet_or_banner, get_selected_wallet};
use crate::ui::theme::DashColors;
use crate::ui::{MessageType, Screen, ScreenLike, ScreenType};
use dash_sdk::dpp::document::DocumentV0Getters;
Expand Down Expand Up @@ -115,10 +115,10 @@ impl ContactRequests {
.id()
.to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58);

// Get wallet for the selected identity
new_self.selected_wallet = get_selected_wallet(&preferred, Some(&app_context), None)
.or_show_error(app_context.egui_ctx())
.unwrap_or(None);
new_self.selected_wallet = auto_selected_wallet_or_banner(
app_context.egui_ctx(),
get_selected_wallet(&preferred, Some(&app_context), None),
);
}

new_self
Expand Down
9 changes: 5 additions & 4 deletions src/ui/dashpay/profile_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use crate::ui::components::wallet_unlock_popup::{
};
use crate::ui::components::{MessageBanner, ResultBannerExt};
use crate::ui::helpers::{ModalOpeningGuard, clicked_outside_window_after_open};
use crate::ui::identities::get_selected_wallet;
use crate::ui::identities::{auto_selected_wallet_or_banner, get_selected_wallet};
use crate::ui::state::AvatarCache;
use crate::ui::theme::{ComponentStyles, DashColors, ResponseExt};
use dash_sdk::dpp::identity::accessors::IdentityGettersV0;
Expand Down Expand Up @@ -134,9 +134,10 @@ impl ProfileScreen {
new_self.selected_identity_string
);

new_self.selected_wallet = get_selected_wallet(&preferred, Some(&app_context), None)
.or_show_error(app_context.egui_ctx())
.unwrap_or(None);
new_self.selected_wallet = auto_selected_wallet_or_banner(
app_context.egui_ctx(),
get_selected_wallet(&preferred, Some(&app_context), None),
);
}

new_self
Expand Down
10 changes: 5 additions & 5 deletions src/ui/dashpay/qr_code_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::ui::components::wallet_unlock_popup::{
use crate::ui::components::{MessageBanner, ResultBannerExt};
use crate::ui::dashpay::dashpay_screen::DashPaySubscreen;
use crate::ui::identities::funding_common::generate_qr_code_image;
use crate::ui::identities::get_selected_wallet;
use crate::ui::identities::{auto_selected_wallet_or_banner, get_selected_wallet};
use crate::ui::theme::DashColors;
use crate::ui::{MessageType, RootScreenType, ScreenLike};
use eframe::epaint::TextureHandle;
Expand Down Expand Up @@ -82,10 +82,10 @@ impl QRCodeGeneratorScreen {
new_self.selected_identity = Some(preferred.clone());
new_self.selected_identity_string = preferred.identity.id().to_string(Encoding::Base58);

// Get wallet for the selected identity
new_self.selected_wallet = get_selected_wallet(&preferred, Some(&app_context), None)
.or_show_error(app_context.egui_ctx())
.unwrap_or(None);
new_self.selected_wallet = auto_selected_wallet_or_banner(
app_context.egui_ctx(),
get_selected_wallet(&preferred, Some(&app_context), None),
);
}

new_self
Expand Down
166 changes: 158 additions & 8 deletions src/ui/identities/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
use std::error::Error as StdError;
use std::fmt;
use std::sync::{Arc, RwLock};

use dash_sdk::{
dpp::data_contract::accessors::v0::DataContractV0Getters, platform::IdentityPublicKey,
dpp::{
data_contract::accessors::v0::DataContractV0Getters,
data_contract::errors::DataContractError,
},
platform::IdentityPublicKey,
};

use crate::{
context::AppContext,
model::{qualified_identity::QualifiedIdentity, wallet::Wallet},
ui::{MessageType, components::MessageBanner},
};

pub mod add_existing_identity_screen;
Expand All @@ -19,6 +26,81 @@ pub mod top_up_identity_screen;
pub mod transfer_screen;
pub mod withdraw_screen;

#[derive(Debug)]
pub enum SelectedWalletError {
DpnsPreorderDocumentTypeNotFound {
source: DataContractError,
},
MissingDocumentSigningKey {
identity_label: Option<String>,
identity_id: String,
},
NoKeyProvided,
}

impl fmt::Display for SelectedWalletError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::DpnsPreorderDocumentTypeNotFound { .. } => {
write!(f, "DPNS preorder document type not found")
}
Self::MissingDocumentSigningKey {
identity_label,
identity_id,
} => {
if let Some(identity_label) = identity_label {
write!(
f,
"Identity {identity_label} ({identity_id}) cannot sign this action \
because it has no high-security signing key. Add a high-security \
signing key to this identity, or choose a different identity."
)
} else {
write!(
f,
"Identity {identity_id} cannot sign this action because it has no \
high-security signing key. Add a high-security signing key to this \
identity, or choose a different identity."
)
}
Comment thread
thepastaclaw marked this conversation as resolved.
}
Self::NoKeyProvided => write!(f, "No key provided when getting selected wallet"),
}
}
}

impl StdError for SelectedWalletError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self {
Self::DpnsPreorderDocumentTypeNotFound { source } => Some(source),
Self::MissingDocumentSigningKey { .. } | Self::NoKeyProvided => None,
}
}
}

/// Returns `true` if the given error is the expected "identity has no
/// document-signing key" error from [`get_selected_wallet`].
pub fn is_missing_document_signing_key_error(error: &SelectedWalletError) -> bool {
matches!(error, SelectedWalletError::MissingDocumentSigningKey { .. })
}

/// Applies the auto-selection policy for screen construction:
/// suppress the expected missing-document-signing-key error for
/// auto-selected identities, but surface every other failure.
pub fn auto_selected_wallet_or_banner(
ctx: &egui::Context,
result: Result<Option<Arc<RwLock<Wallet>>>, SelectedWalletError>,
) -> Option<Arc<RwLock<Wallet>>> {
match result {
Ok(wallet) => wallet,
Err(error) if is_missing_document_signing_key_error(&error) => None,
Err(error) => {
MessageBanner::set_global(ctx, &error, MessageType::Error);
None
}
}
}

/// Retrieves the appropriate wallet (if any) associated with the given identity.
///
/// # Description
Expand All @@ -43,8 +125,8 @@ pub mod withdraw_screen;
/// # Returns
///
/// Returns `Ok(Some(Arc<RwLock<Wallet>>))` if a matching wallet is found,
/// `Ok(None)` if no wallet is associated with the key, or `Err(String)` if
/// an error is encountered.
/// `Ok(None)` if no wallet is associated with the key, or
/// `Err(SelectedWalletError)` if an error is encountered.
///
/// # Errors
///
Expand All @@ -55,26 +137,36 @@ pub fn get_selected_wallet(
qualified_identity: &QualifiedIdentity,
app_context: Option<&AppContext>,
selected_key: Option<&IdentityPublicKey>,
) -> Result<Option<Arc<RwLock<Wallet>>>, String> {
) -> Result<Option<Arc<RwLock<Wallet>>>, SelectedWalletError> {
// If `app_context` is provided, use the DPNS-based approach.
let public_key = if let Some(context) = app_context {
let dpns_contract = &context.dpns_contract;

// Attempt to fetch the `preorder` document type from the DPNS contract.
let preorder_document_type = dpns_contract
.document_type_for_name("preorder")
.map_err(|e| format!("DPNS preorder document type not found: {}", e))?;
.map_err(|source| SelectedWalletError::DpnsPreorderDocumentTypeNotFound { source })?;

// Attempt to retrieve the public key from the identity.
qualified_identity
.document_signing_key(&preorder_document_type)
.ok_or_else(|| {
"Identity doesn't have an authentication key for signing document transitions"
.to_string()
use dash_sdk::dpp::identity::accessors::IdentityGettersV0;
use dash_sdk::dpp::platform_value::string_encoding::Encoding;
let identity_label = qualified_identity.alias.as_deref().or_else(|| {
qualified_identity
.dpns_names
.first()
.map(|n| n.name.as_str())
});
SelectedWalletError::MissingDocumentSigningKey {
identity_label: identity_label.map(str::to_owned),
identity_id: qualified_identity.identity.id().to_string(Encoding::Base58),
}
})?
Comment thread
thepastaclaw marked this conversation as resolved.
} else {
// Fallback: directly use the provided selected key.
selected_key.ok_or_else(|| "No key provided when getting selected wallet".to_string())?
selected_key.ok_or(SelectedWalletError::NoKeyProvided)?
};

// Once we have the public key (either from DPNS or directly), ask which
Expand Down Expand Up @@ -186,4 +278,62 @@ mod tests {
"the wallet deriving this key must be found whichever placement names it"
);
}

fn missing_document_signing_key_error(identity_label: Option<&str>) -> SelectedWalletError {
SelectedWalletError::MissingDocumentSigningKey {
identity_label: identity_label.map(str::to_owned),
identity_id: "TestIdentityId".to_string(),
}
}

#[test]
fn missing_document_signing_key_display_includes_label_when_present() {
let error = missing_document_signing_key_error(Some("Test Identity"));

assert_eq!(
error.to_string(),
"Identity Test Identity (TestIdentityId) cannot sign this action because it has no \
high-security signing key. Add a high-security signing key to this identity, or \
choose a different identity."
);
}

#[test]
fn missing_document_signing_key_display_uses_identity_id_without_label() {
let error = missing_document_signing_key_error(None);

assert_eq!(
error.to_string(),
"Identity TestIdentityId cannot sign this action because it has no high-security \
signing key. Add a high-security signing key to this identity, or choose a \
different identity."
);
}

#[test]
fn auto_selection_policy_suppresses_missing_document_signing_key() {
let ctx = egui::Context::default();

let wallet =
auto_selected_wallet_or_banner(&ctx, Err(missing_document_signing_key_error(None)));

assert!(wallet.is_none());
assert!(
!MessageBanner::has_global(&ctx),
"automatic selection must suppress the typed missing-document-signing-key banner"
);
}

#[test]
fn auto_selection_policy_surfaces_unrelated_selected_wallet_error() {
let ctx = egui::Context::default();

let wallet = auto_selected_wallet_or_banner(&ctx, Err(SelectedWalletError::NoKeyProvided));

assert!(wallet.is_none());
assert!(
MessageBanner::has_global(&ctx),
"automatic selection must only suppress MissingDocumentSigningKey"
);
}
}
2 changes: 1 addition & 1 deletion src/ui/tokens/set_token_price_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ impl SetTokenPriceScreen {
let selected_wallet =
get_selected_wallet(&identity_token_info.identity, None, possible_key).unwrap_or_else(
|e| {
set_error_banner(&e);
set_error_banner(&e.to_string());
None
},
);
Expand Down
2 changes: 1 addition & 1 deletion src/ui/tokens/token_action_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ impl<A: TokenAction> TokenActionScreen<A> {
let selected_wallet =
get_selected_wallet(&identity_token_info.identity, None, possible_key.as_ref())
.unwrap_or_else(|e| {
super::set_error_banner(app_context, &e);
super::set_error_banner(app_context, &e.to_string());
None
});

Expand Down
Loading
Loading