Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
42 changes: 40 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,26 @@ use tokio::time::{interval, Duration};
pub static SETTINGS: OnceLock<Settings> = OnceLock::new();

/// Applies one [`OperationResult`] from the background task channel (save attachment, orders, etc.).
/// Results that must re-run the startup DB-to-UI sync (maker book cache +
/// order history messages) because background work changed SQLite rows the
/// in-memory projections are built from.
fn requires_db_projection_resync(result: &OperationResult) -> bool {
matches!(
result,
OperationResult::OrderHistoryDeleted { .. } | OperationResult::SessionRestored { .. }
)
}

async fn apply_order_result(pool: &SqlitePool, app: &mut AppState, result: OperationResult) {
let is_dispute_related = matches!(&result, OperationResult::Info(msg)
if (msg.contains("Dispute") && msg.contains("taken successfully"))
|| msg.contains("Dispute finalized"));
let resync_my_trades_from_db = matches!(&result, OperationResult::OrderHistoryDeleted { .. });
let resync_my_trades_from_db = requires_db_projection_resync(&result);
let refresh_maker_book_cache = matches!(
&result,
OperationResult::MyTradesMakerBookChanged | OperationResult::Success(_)
OperationResult::MyTradesMakerBookChanged
| OperationResult::Success(_)
| OperationResult::SessionRestored { .. }
);

if refresh_maker_book_cache && app.user_role == UserRole::User {
Expand Down Expand Up @@ -793,3 +805,29 @@ async fn main() -> Result<(), anyhow::Error> {

Ok(())
}

#[cfg(test)]
mod apply_order_result_tests {
use super::requires_db_projection_resync;
use crate::ui::OperationResult;

#[test]
fn session_restore_triggers_the_startup_db_resync() {
// Regression: a restore rewrites SQLite from a background task; without
// the resync the recovered orders stay invisible until app restart.
assert!(requires_db_projection_resync(
&OperationResult::SessionRestored {
message: String::new()
}
));
assert!(requires_db_projection_resync(
&OperationResult::OrderHistoryDeleted {
deleted_order_ids: vec![],
message: String::new()
}
));
assert!(!requires_db_projection_resync(&OperationResult::Info(
String::new()
)));
}
}
3 changes: 3 additions & 0 deletions src/ui/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ pub enum UiMode {
ConfirmClearCurrencies(bool), // (selected_button: true=Yes, false=No)
ConfirmDeleteHistoryOrder(uuid::Uuid, bool), // (order_id, selected_button)
ConfirmBulkDeleteHistory(bool), // (selected_button)
/// User Settings: ask Mostro to restore this identity's orders and disputes.
ConfirmRestoreSession(bool), // (selected_button: true=Yes, false=No)
ConfirmExit(bool), // (selected_button: true=Yes, false=No)

// Generate new keys flow (Settings tab)
Expand Down Expand Up @@ -148,6 +150,7 @@ impl Clone for UiMode {
UiMode::ConfirmBulkDeleteHistory(selected) => {
UiMode::ConfirmBulkDeleteHistory(*selected)
}
UiMode::ConfirmRestoreSession(selected) => UiMode::ConfirmRestoreSession(*selected),
UiMode::ConfirmExit(selected) => UiMode::ConfirmExit(*selected),
UiMode::ConfirmGenerateNewKeys(selected) => UiMode::ConfirmGenerateNewKeys(*selected),
// Clamp cloning of secret mnemonic to avoid duplicating sensitive seed words.
Expand Down
9 changes: 9 additions & 0 deletions src/ui/draw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,15 @@ No: paste BOLT11 or Lightning address manually."
Some("Delete selected terminal order from local database history?"),
);
}
if let UiMode::ConfirmRestoreSession(selected_button) = &app.mode {
admin_key_confirm::render_admin_key_confirm_with_message(
f,
"\u{1F504} Restore Session",
"",
*selected_button,
Some("Ask Mostro to restore this identity's orders and disputes into the local database?"),
);
}
if let UiMode::ConfirmBulkDeleteHistory(selected_button) = &app.mode {
admin_key_confirm::render_admin_key_confirm_with_message(
f,
Expand Down
4 changes: 4 additions & 0 deletions src/ui/help_popup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,10 @@ fn settings_instruction_lines(user_role: UserRole) -> (String, Vec<Line<'static>
"View Seed Words",
"Show your BIP-39 mnemonic from the local database. Treat as highly sensitive.",
),
(
"Restore Session (from Mostro)",
"Recover this identity's orders and disputes from Mostro after a reinstall or on a new machine.",
),
Comment thread
grunch marked this conversation as resolved.
(
"Generate New Keys",
"Rotate identity/trade keys. Confirm prompts and back up any new mnemonic.",
Expand Down
42 changes: 41 additions & 1 deletion src/ui/key_handler/enter_handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ use crate::ui::key_handler::validation::{
};
use crate::ui::tabs::settings_tab::{settings_action_for_index, SettingsMenuAction};
use crate::util::dm_utils::{apply_saved_ln_address_invoice_choice, present_add_invoice_popup};
use crate::util::order_utils::BondSlashChoice;
use crate::util::order_utils::{execute_restore_session, BondSlashChoice};

fn invoice_popup_action_for_message_action(action: &Action) -> Option<Action> {
match action {
Expand Down Expand Up @@ -567,6 +567,43 @@ pub fn handle_enter_key(app: &mut AppState, ctx: &super::EnterKeyContext<'_>) ->
}
true
}
UiMode::ConfirmRestoreSession(selected_button) => {
if selected_button {
app.mode = UiMode::operation_result(OperationResult::Info(
"Restoring session from Mostro...".to_string(),
));
let pool = ctx.pool.clone();
let client = ctx.client.clone();
let mostro_pubkey = ctx.mostro_pubkey;
let mostro_info = ctx.mostro_info.clone();
let result_tx = ctx.order_result_tx.clone();
let dm_subscription_tx = ctx.dm_subscription_tx.clone();
tokio::spawn(async move {
match execute_restore_session(
&pool,
&client,
mostro_pubkey,
mostro_info.as_ref(),
dm_subscription_tx,
)
.await
{
Ok(summary) => {
let _ =
result_tx.send(OperationResult::Info(summary.to_user_message()));
Comment thread
grunch marked this conversation as resolved.
Outdated
}
Err(e) => {
log::error!("Session restore failed: {e}");
let _ = result_tx
.send(OperationResult::Error(format!("Restore failed: {e}")));
}
}
});
} else {
app.mode = default_mode;
}
true
}
UiMode::ConfirmGenerateNewKeys(selected_button) => {
if !selected_button {
// NO: just close warning popup.
Expand Down Expand Up @@ -1251,6 +1288,9 @@ fn handle_enter_normal_mode(app: &mut AppState, ctx: &super::EnterKeyContext<'_>
Some(SettingsMenuAction::ChangeAdminKey) => {
app.mode = UiMode::AdminMode(AdminMode::SetupAdminKey(key_state));
}
Some(SettingsMenuAction::RestoreSession) => {
app.mode = UiMode::ConfirmRestoreSession(true);
}
Some(SettingsMenuAction::GenerateNewKeys) => {
app.mode = UiMode::ConfirmGenerateNewKeys(true);
}
Expand Down
4 changes: 3 additions & 1 deletion src/ui/key_handler/esc_handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ pub fn handle_esc_key(app: &mut AppState) -> bool {
app.mode = default_mode.clone();
true
}
UiMode::ConfirmDeleteHistoryOrder(_, _) | UiMode::ConfirmBulkDeleteHistory(_) => {
UiMode::ConfirmDeleteHistoryOrder(_, _)
| UiMode::ConfirmBulkDeleteHistory(_)
| UiMode::ConfirmRestoreSession(_) => {
app.mode = default_mode.clone();
true
}
Expand Down
1 change: 1 addition & 0 deletions src/ui/key_handler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,7 @@ pub fn handle_key_event(
| UiMode::ConfirmClearCurrencies(ref mut selected_button)
| UiMode::ConfirmDeleteHistoryOrder(_, ref mut selected_button)
| UiMode::ConfirmBulkDeleteHistory(ref mut selected_button)
| UiMode::ConfirmRestoreSession(ref mut selected_button)
| UiMode::ConfirmGenerateNewKeys(ref mut selected_button)
| UiMode::ConfirmExit(ref mut selected_button) => {
*selected_button = !*selected_button; // Toggle between YES and NO
Expand Down
4 changes: 4 additions & 0 deletions src/ui/key_handler/navigation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ fn handle_left_key(app: &mut AppState, _orders: &Arc<Mutex<Vec<SmallOrder>>>) {
| UiMode::ConfirmClearCurrencies(ref mut selected_button)
| UiMode::ConfirmDeleteHistoryOrder(_, ref mut selected_button)
| UiMode::ConfirmBulkDeleteHistory(ref mut selected_button)
| UiMode::ConfirmRestoreSession(ref mut selected_button)
| UiMode::ConfirmExit(ref mut selected_button) => {
// Switch to YES button (left side)
*selected_button = true;
Expand Down Expand Up @@ -157,6 +158,7 @@ fn handle_right_key(app: &mut AppState, _orders: &Arc<Mutex<Vec<SmallOrder>>>) {
| UiMode::ConfirmClearCurrencies(ref mut selected_button)
| UiMode::ConfirmDeleteHistoryOrder(_, ref mut selected_button)
| UiMode::ConfirmBulkDeleteHistory(ref mut selected_button)
| UiMode::ConfirmRestoreSession(ref mut selected_button)
| UiMode::ConfirmExit(ref mut selected_button) => {
// Switch to NO button (right side)
*selected_button = false;
Expand Down Expand Up @@ -307,6 +309,7 @@ fn handle_up_key(
| UiMode::ConfirmClearCurrencies(_)
| UiMode::ConfirmDeleteHistoryOrder(_, _)
| UiMode::ConfirmBulkDeleteHistory(_)
| UiMode::ConfirmRestoreSession(_)
| UiMode::ConfirmGenerateNewKeys(_)
| UiMode::BackupNewKeys(_)
| UiMode::ConfirmExit(_) => {
Expand Down Expand Up @@ -466,6 +469,7 @@ fn handle_down_key(
| UiMode::ConfirmClearCurrencies(_)
| UiMode::ConfirmDeleteHistoryOrder(_, _)
| UiMode::ConfirmBulkDeleteHistory(_)
| UiMode::ConfirmRestoreSession(_)
| UiMode::ConfirmGenerateNewKeys(_)
| UiMode::BackupNewKeys(_)
| UiMode::ConfirmExit(_) => {
Expand Down
2 changes: 2 additions & 0 deletions src/ui/operation_result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ pub fn render_operation_result(f: &mut ratatui::Frame, result: &OperationResult)
| OperationResult::ObserverChatLoaded(_)
| OperationResult::ObserverChatError(_) => 8,
OperationResult::Info(message) => info_popup_height(message, popup_width),
OperationResult::SessionRestored { message } => info_popup_height(message, popup_width),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
OperationResult::Error(_)
| OperationResult::InvoiceSubmitted { .. }
| OperationResult::TradeClosed { .. }
Expand Down Expand Up @@ -203,6 +204,7 @@ pub fn render_operation_result(f: &mut ratatui::Frame, result: &OperationResult)
f.render_widget(paragraph, inner);
}
OperationResult::Info(message)
| OperationResult::SessionRestored { message }
| OperationResult::InvoiceSubmitted { message, .. }
| OperationResult::TradeClosed { message, .. }
| OperationResult::OrderHistoryDeleted { message, .. } => {
Expand Down
5 changes: 5 additions & 0 deletions src/ui/orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,11 @@ pub enum OperationResult {
},
/// Rebuild [`crate::ui::AppState::my_trades_maker_book`] from SQLite (no UI popup).
MyTradesMakerBookChanged,
/// Session restore finished: resync My Trades/Messages projections from
/// SQLite (same DB-to-UI sync as startup), then show `message`.
SessionRestored {
message: String,
},
/// Open invoice / waiting popup from a synchronous execute reply (e.g. bond payout DM).
OpenInvoicePopup {
notification: MessageNotification,
Expand Down
39 changes: 38 additions & 1 deletion src/ui/tabs/settings_tab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub enum SettingsMenuAction {
AddCurrencyFilter,
ClearCurrencyFilters,
ViewSeedWords,
RestoreSession,
AddDisputeSolver,
ChangeAdminKey,
GenerateNewKeys,
Expand Down Expand Up @@ -44,7 +45,7 @@ const ADMIN_SETTINGS: [SettingsMenuRow; 9] = [

/// Single source of truth for User Settings rows (action + list label).
#[allow(clippy::redundant_static_lifetimes)]
const USER_SETTINGS: [SettingsMenuRow; 9] = [
const USER_SETTINGS: [SettingsMenuRow; 10] = [
(SettingsMenuAction::SwitchMode, "Switch Mode (User ↔ Admin)"),
(
SettingsMenuAction::ChangeMostroPubkey,
Expand All @@ -65,6 +66,10 @@ const USER_SETTINGS: [SettingsMenuRow; 9] = [
"Clear Currency Filters",
),
(SettingsMenuAction::ViewSeedWords, "View Seed Words"),
(
SettingsMenuAction::RestoreSession,
"Restore Session (from Mostro)",
),
(SettingsMenuAction::GenerateNewKeys, "Generate New Keys"),
];

Expand Down Expand Up @@ -198,3 +203,35 @@ pub fn render_settings_tab(
chunks[4],
);
}

#[cfg(test)]
mod settings_menu_tests {
use super::*;

#[test]
fn restore_session_is_a_user_option_but_not_an_admin_one() {
assert!(USER_SETTINGS
.iter()
.any(|(a, _)| *a == SettingsMenuAction::RestoreSession));
// Admin mode signs with admin_privkey, not the identity mnemonic Mostro
// indexes users by, so a restore there would recover nothing.
assert!(!ADMIN_SETTINGS
.iter()
.any(|(a, _)| *a == SettingsMenuAction::RestoreSession));
}

#[test]
fn user_menu_keeps_restore_next_to_the_key_management_rows() {
let labels: Vec<&str> = USER_SETTINGS.iter().map(|(_, l)| *l).collect();
let seed = labels.iter().position(|l| *l == "View Seed Words").unwrap();
let restore = labels
.iter()
.position(|l| *l == "Restore Session (from Mostro)")
.unwrap();
let generate = labels
.iter()
.position(|l| *l == "Generate New Keys")
.unwrap();
assert!(seed < restore && restore < generate);
}
}
3 changes: 3 additions & 0 deletions src/util/dm_utils/order_ch_mng.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ pub fn handle_operation_result(mut result: OperationResult, app: &mut AppState)
remove_many_orders_from_messages_tab(app, &deleted_order_ids);
result = OperationResult::Info(message);
}
if let OperationResult::SessionRestored { message } = result {
result = OperationResult::Info(message);
}
if let OperationResult::InvoiceSubmitted {
message,
remember_buyer_saved_ln_address_for_order,
Expand Down
Loading