Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ca989cd
fix: replace expect() on ZMQ listener creation with graceful error ha…
PastaPastaPasta Feb 12, 2026
c03dfab
fix: replace unimplemented!/todo!/panic! macros with proper error han…
PastaPastaPasta Feb 12, 2026
bdbb30b
fix: replace unwrap/expect on database initialization with proper err…
PastaPastaPasta Feb 12, 2026
782b376
fix: replace expect() calls with proper error handling in platform_in…
PastaPastaPasta Feb 12, 2026
554d416
fix: replace panic on DB migration failure with error propagation
PastaPastaPasta Feb 12, 2026
8cf695a
fix: replace 6 panicking calls in asset lock DB loading with error pr…
PastaPastaPasta Feb 12, 2026
a786360
fix: replace expect() panics in current_app_context() with mainnet fa…
PastaPastaPasta Feb 12, 2026
c8707fc
fix: replace unwrap() on Identifier::from_bytes in contacts.rs
PastaPastaPasta Feb 12, 2026
1bb6fbb
fix: replace expect() panics with error propagation in contested names
PastaPastaPasta Feb 12, 2026
bb55e15
fix: replace panicking unwrap/expect in spawned contested resources t…
PastaPastaPasta Feb 12, 2026
6e13205
fix: replace expect() panics with error propagation in identity/top-u…
PastaPastaPasta Feb 12, 2026
dcbc322
fix: replace expect() with warning+skip for missing token configuration
PastaPastaPasta Feb 12, 2026
aa89784
fix: replace expect() panics with error propagation in database/walle…
PastaPastaPasta Feb 12, 2026
f8cd3b9
fix: replace expect() panics with error propagation in database/conte…
PastaPastaPasta Feb 12, 2026
03b1588
fix: replace SystemTime expect/unwrap panics with unwrap_or_default
PastaPastaPasta Feb 12, 2026
ad2c977
Replace unreachable!() in scheduled_votes.rs with safe fallback
PastaPastaPasta Feb 12, 2026
f00f08a
Fix silent error masking in contacts.rs load_contact_private_info
PastaPastaPasta Feb 12, 2026
845d002
fix: replace expect() on config address parsing and SDK initializatio…
PastaPastaPasta Feb 12, 2026
7d255fe
fix: replace expect() panics with graceful error handling in AppContext
PastaPastaPasta Feb 12, 2026
18e6f6f
fix: replace expect() panics with error propagation in asset lock pro…
PastaPastaPasta Feb 12, 2026
fd70bd1
fix: replace unimplemented! macro and revert unrelated lock changes
PastaPastaPasta Feb 12, 2026
ba16884
fix: address PR review findings from CodeRabbit, Copilot, and lklimek
PastaPastaPasta Feb 12, 2026
8e20c90
fix: use FromSqlConversionFailure for column data errors and remove s…
PastaPastaPasta Feb 16, 2026
b9d88af
fix: resolve CodeRabbit network fallback and withdrawal parsing findings
PastaPastaPasta Feb 19, 2026
5bc0133
style: fix rustfmt in platform info
PastaPastaPasta Feb 19, 2026
424b2ca
fix: prevent silent mainnet fallback in current app context
PastaPastaPasta Feb 19, 2026
80b2b55
fix: remove automatic network fallback behavior
PastaPastaPasta Feb 19, 2026
0fc37c6
fix: remove unused Color32 import
PastaPastaPasta Feb 19, 2026
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
216 changes: 153 additions & 63 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,42 +165,30 @@ impl BitOrAssign for AppAction {
}
}
impl AppState {
pub fn new(ctx: egui::Context) -> Self {
create_app_user_data_directory_if_not_exists()
.expect("Failed to create app user_data directory");
pub fn new(ctx: egui::Context) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
create_app_user_data_directory_if_not_exists()?;
copy_env_file_if_not_exists();
initialize_logger();
let db_file_path = app_user_data_file_path("data.db").expect("should create db file path");
let db = Arc::new(Database::new(&db_file_path).unwrap());
db.initialize(&db_file_path).unwrap();
let db_file_path = app_user_data_file_path("data.db")?;
let db = Arc::new(Database::new(&db_file_path)?);
db.initialize(&db_file_path)?;

let settings = db
.get_settings()
.expect("expected to get settings")
.map(Settings::from)
.unwrap_or_default();
let settings = db.get_settings()?.map(Settings::from).unwrap_or_default();
let password_info = settings.password_info;
let theme_preference = settings.theme_mode;
let overwrite_dash_conf = settings.overwrite_dash_conf;
let onboarding_completed = settings.onboarding_completed;

let subtasks = Arc::new(TaskManager::new());
let connection_status = Arc::new(ConnectionStatus::new());
let mainnet_app_context = match AppContext::new(
let mainnet_app_context = AppContext::new(
Network::Dash,
db.clone(),
password_info.clone(),
subtasks.clone(),
connection_status.clone(),
) {
Some(context) => context,
None => {
tracing::error!(
"Failed to create the AppContext. Expected Dash config for mainnet."
);
std::process::exit(1);
}
};
)
.ok_or("Failed to create AppContext for mainnet. Check your Dash configuration.")?;
let testnet_app_context = AppContext::new(
Network::Testnet,
db.clone(),
Expand Down Expand Up @@ -279,7 +267,38 @@ impl AppState {
let mut wallets_balances_screen = WalletsBalancesScreen::new(&mainnet_app_context);

let selected_main_screen = settings.root_screen_type;
let chosen_network = settings.network;
// Validate that the saved network has an available context.
// We fail fast instead of silently routing user actions to a different network.
let chosen_network = match settings.network {
Network::Dash => Network::Dash,
Network::Testnet => {
assert!(
testnet_app_context.is_some(),
"Saved network is Testnet but no Testnet AppContext is configured"
);
Network::Testnet
}
Network::Devnet => {
assert!(
devnet_app_context.is_some(),
"Saved network is Devnet but no Devnet AppContext is configured"
);
Network::Devnet
}
Network::Regtest => {
assert!(
local_app_context.is_some(),
"Saved network is Regtest but no Regtest AppContext is configured"
);
Network::Regtest
}
unsupported_network => {
panic!(
"Saved network {:?} is unsupported. Refusing automatic fallback.",
unsupported_network
);
}
};
network_chooser_screen.current_network = chosen_network;

if let (Network::Testnet, Some(testnet_app_context)) =
Expand Down Expand Up @@ -411,15 +430,21 @@ impl AppState {
.map(|s| s.disable_zmq)
.unwrap_or(false);
let mainnet_core_zmq_listener = if !mainnet_disable_zmq {
Some(
CoreZMQListener::spawn_listener(
Network::Dash,
&mainnet_core_zmq_endpoint,
core_message_sender.clone(), // Clone the sender for each listener
Some(mainnet_app_context.sx_zmq_status.clone()),
)
.expect("Failed to create mainnet InstantSend listener"),
)
match CoreZMQListener::spawn_listener(
Network::Dash,
&mainnet_core_zmq_endpoint,
core_message_sender.clone(),
Some(mainnet_app_context.sx_zmq_status.clone()),
) {
Ok(listener) => Some(listener),
Err(e) => {
tracing::error!(
"Failed to create mainnet ZMQ listener: {}. ZMQ features will be unavailable for mainnet.",
e
);
None
}
}
} else {
None
};
Expand All @@ -438,15 +463,21 @@ impl AppState {
.map(|s| s.disable_zmq)
.unwrap_or(false);
let testnet_core_zmq_listener = if !testnet_disable_zmq {
Some(
CoreZMQListener::spawn_listener(
Network::Testnet,
&testnet_core_zmq_endpoint,
core_message_sender.clone(), // Use the original sender or create a new one if needed
testnet_tx_zmq_status_option,
)
.expect("Failed to create testnet InstantSend listener"),
)
match CoreZMQListener::spawn_listener(
Network::Testnet,
&testnet_core_zmq_endpoint,
core_message_sender.clone(),
testnet_tx_zmq_status_option,
) {
Ok(listener) => Some(listener),
Err(e) => {
tracing::error!(
"Failed to create testnet ZMQ listener: {}. ZMQ features will be unavailable for testnet.",
e
);
None
}
}
} else {
None
};
Expand All @@ -465,15 +496,21 @@ impl AppState {
.map(|s| s.disable_zmq)
.unwrap_or(false);
let devnet_core_zmq_listener = if !devnet_disable_zmq {
Some(
CoreZMQListener::spawn_listener(
Network::Devnet,
&devnet_core_zmq_endpoint,
core_message_sender.clone(),
devnet_tx_zmq_status_option,
)
.expect("Failed to create devnet InstantSend listener"),
)
match CoreZMQListener::spawn_listener(
Network::Devnet,
&devnet_core_zmq_endpoint,
core_message_sender.clone(),
devnet_tx_zmq_status_option,
) {
Ok(listener) => Some(listener),
Err(e) => {
tracing::error!(
"Failed to create devnet ZMQ listener: {}. ZMQ features will be unavailable for devnet.",
e
);
None
}
}
} else {
None
};
Expand All @@ -492,15 +529,21 @@ impl AppState {
.map(|s| s.disable_zmq)
.unwrap_or(false);
let local_core_zmq_listener = if !local_disable_zmq {
Some(
CoreZMQListener::spawn_listener(
Network::Regtest,
&local_core_zmq_endpoint,
core_message_sender,
local_tx_zmq_status_option,
)
.expect("Failed to create local InstantSend listener"),
)
match CoreZMQListener::spawn_listener(
Network::Regtest,
&local_core_zmq_endpoint,
core_message_sender,
local_tx_zmq_status_option,
) {
Ok(listener) => Some(listener),
Err(e) => {
tracing::error!(
"Failed to create local ZMQ listener: {}. ZMQ features will be unavailable for local/regtest.",
e
);
None
}
}
} else {
None
};
Expand Down Expand Up @@ -662,7 +705,7 @@ impl AppState {
}
}

app_state
Ok(app_state)
}

/// Allows enabling or disabling animations globally for the app.
Expand All @@ -684,15 +727,53 @@ impl AppState {
}

pub fn current_app_context(&self) -> &Arc<AppContext> {
// Invariant: chosen_network must always have a corresponding context.
// Fail fast on violations to avoid silently routing operations to mainnet.
match self.chosen_network {
Network::Dash => &self.mainnet_app_context,
Network::Testnet => self.testnet_app_context.as_ref().expect("expected testnet"),
Network::Devnet => self.devnet_app_context.as_ref().expect("expected devnet"),
Network::Regtest => self.local_app_context.as_ref().expect("expected local"),
_ => todo!(),
Network::Testnet => self.testnet_app_context.as_ref().unwrap_or_else(|| {
panic!(
"BUG: chosen network is Testnet but testnet_app_context is missing; refusing silent mainnet fallback"
)
}),
Network::Devnet => self.devnet_app_context.as_ref().unwrap_or_else(|| {
panic!(
"BUG: chosen network is Devnet but devnet_app_context is missing; refusing silent mainnet fallback"
)
}),
Network::Regtest => self.local_app_context.as_ref().unwrap_or_else(|| {
panic!(
"BUG: chosen network is Regtest but local_app_context is missing; refusing silent mainnet fallback"
)
}),
unsupported_network => panic!(
"BUG: unsupported network variant {:?} in current_app_context; refusing silent mainnet fallback",
unsupported_network
),
}
}

fn context_available_for_network(&self, network: Network) -> bool {
match network {
Network::Dash => true, // Mainnet is always available
Network::Testnet => self.testnet_app_context.is_some(),
Network::Devnet => self.devnet_app_context.is_some(),
Network::Regtest => self.local_app_context.is_some(),
_ => false,
}
}

fn enforce_network_context_invariant(&mut self) {
if self.context_available_for_network(self.chosen_network) {
return;
}

panic!(
"BUG: selected network {:?} has no AppContext. Refusing to auto-switch networks.",
self.chosen_network
);
}

// Handle the backend task and send the result through the channel
fn handle_backend_task(&self, task: BackendTask) {
let sender = self.task_result_sender.clone();
Expand Down Expand Up @@ -742,6 +823,14 @@ impl AppState {
}

pub fn change_network(&mut self, network: Network) {
if !self.context_available_for_network(network) {
tracing::error!(
"Cannot switch to {:?}: network context not available. Staying on current network.",
network
);
return;
}

self.chosen_network = network;
let app_context = self.current_app_context().clone();

Expand Down Expand Up @@ -802,6 +891,7 @@ impl App for AppState {
// Apply Dash theme with user preference
crate::ui::theme::apply_theme(ctx, self.theme_preference);

self.enforce_network_context_invariant();
let active_context = self.current_app_context().clone();

// Poll the receiver for any new task results
Expand Down
11 changes: 8 additions & 3 deletions src/app_dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,20 @@ pub fn core_cookie_path(
network: Network,
devnet_name: &Option<String>,
) -> Result<PathBuf, std::io::Error> {
core_user_data_dir_path().map(|path| {
core_user_data_dir_path().and_then(|path| {
let network_dir = match network {
Network::Dash => "",
Network::Testnet => "testnet3",
Network::Devnet => devnet_name.as_deref().unwrap_or(""),
Network::Regtest => "regtest",
_ => unimplemented!(),
_ => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Unsupported network for cookie path: {:?}", network),
));
}
};
path.join(network_dir).join(".cookie")
Ok(path.join(network_dir).join(".cookie"))
})
}

Expand Down
Loading
Loading