Repository: wizardsardine/liana (rust)

Issue:
[GUI] Default size of Liana should allow avoiding to scroll most screens
The default size of Liana application window (not the minimum, which should be unchanged) should be big enough for the main pages of the installer and wallet itself to not be scrolled. For example, with the current size the introduction pages of the templates in the installer and the "Send" page need often to be scrolled. The default size should avoid this as much as possible (even if for the Send page this depends on the number of coins present, which would suggest a review of its structure).

Pull request description:
Smart screen size
closes #1546 following last requirements:

> Currently the consensus has been on the following solution: resize the window according to a logic similar to https://github.com/wizardsardine/liana/pull/1695 at the first launch and then remember the last size for following launches of the application.

Summary of the feature introduced in this PR:
- The first time Liana launch, the window is maximized in order to "guess" the maximum usable size on the monitor, and then we apply [this logic](https://github.com/wizardsardine/liana/issues/1546#issuecomment-2750974470) for deciding the default size.
- The default size is recorded in `global_settings.json` (so this aplly to ALL wallets)
- On nexts Liana launch, the window is resized to the value contained in `global_settings.json`

I added something not clearly defined: if the user resize its screen, we reapply the last screen size at startup.

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 44b58e48..b6f24a6d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1827,6 +1827,16 @@ dependencies = [
  "percent-encoding",
 ]
 
+[[package]]
+name = "fs2"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
 [[package]]
 name = "futures"
 version = "0.3.31"
@@ -3053,6 +3063,7 @@ dependencies = [
  "dirs 3.0.2",
  "email_address",
  "flate2",
+ "fs2",
  "hex",
  "iced",
  "iced_aw",
diff --git a/liana-gui/Cargo.toml b/liana-gui/Cargo.toml
index fe969d87..4affc27c 100644
--- a/liana-gui/Cargo.toml
+++ b/liana-gui/Cargo.toml
@@ -54,6 +54,7 @@ bitcoin_hashes = "0.12"
 reqwest = { version = "0.11", default-features=false, features = ["json", "rustls-tls", "stream"] }
 rust-ini = "0.19.0"
 rfd = "0.15.1"
+fs2 = "0.4.3"
 
 
 [target.'cfg(windows)'.dependencies]
diff --git a/liana-gui/src/app/mod.rs b/liana-gui/src/app/mod.rs
index 03c06431..9d5b44d0 100644
--- a/liana-gui/src/app/mod.rs
+++ b/liana-gui/src/app/mod.rs
@@ -435,6 +435,10 @@ impl App {
             content
         }
     }
+
+    pub fn datadir_path(&self) -> &LianaDirectory {
+        &self.cache.datadir_path
+    }
 }
 
 fn new_recovery_panel(wallet: Arc<Wallet>, cache: &Cache) -> CreateSpendPanel {
diff --git a/liana-gui/src/app/settings.rs b/liana-gui/src/app/settings.rs
index af0ba3fd..9aa269e4 100644
--- a/liana-gui/src/app/settings.rs
+++ b/liana-gui/src/app/settings.rs
@@ -380,18 +380,141 @@ impl std::fmt::Display for SettingsError {
 pub mod global {
     use crate::dir::LianaDirectory;
     use async_hwi::bitbox::{ConfigError, NoiseConfig, NoiseConfigData};
+    use fs2::FileExt;
     use serde::{Deserialize, Serialize};
-    use std::io::{Read, Write};
+    use std::fs::OpenOptions;
+    use std::io::{Read, Seek, SeekFrom, Write};
     use std::path::PathBuf;
 
     pub const DEFAULT_FILE_NAME: &str = "global_settings.json";
 
-    #[derive(Debug, Deserialize, Serialize)]
-    pub struct Settings {
+    #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
+    pub struct WindowConfig {
+        pub width: f32,
+        pub height: f32,
+    }
+
+    #[derive(Debug, Deserialize, Serialize, Default)]
+    pub struct GlobalSettings {
         pub bitbox: Option<BitboxSettings>,
+        pub window_config: Option<WindowConfig>,
+    }
+
+    impl GlobalSettings {
+        pub fn path(global_datadir: &LianaDirectory) -> PathBuf {
+            global_datadir.path().join(DEFAULT_FILE_NAME)
+        }
+
+        pub fn load_window_config(path: &PathBuf) -> Option<WindowConfig> {
+            let mut ret = None;
+            if let Err(e) = Self::update(path, |s| ret = s.window_config.clone(), false) {
+                tracing::error!("Failed to load window config: {e}");
+            }
+            ret
+        }
+
+        pub fn update_window_config(
+            path: &PathBuf,
+            window_config: &WindowConfig,
+        ) -> Result<(), String> {
+            Self::update(
+                path,
+                |s| s.window_config = Some(window_config.clone()),
+                true,
+            )
+        }
+
+        pub fn load_bitbox_settings(path: &PathBuf) -> Result<Option<BitboxSettings>, String> {
+            let mut ret = None;
+            Self::update(path, |s| ret = s.bitbox.clone(), false)?;
+            Ok(ret)
+        }
+
+        pub fn update_bitbox_settings(
+            path: &PathBuf,
+            bitbox: &BitboxSettings,
+        ) -> Result<(), String> {
+            Self::update(path, |s| s.bitbox = Some(bitbox.clone()), true)
+        }
+
+        pub fn update<F>(path: &PathBuf, mut update: F, mut write: bool) -> Result<(), String>
+        where
+            F: FnMut(&mut GlobalSettings),
+        {
+            log::info!("GLobalSettings::update() write: {write}");
+            let exists = path.is_file();
+
+            let (mut global_settings, file) = if exists {
+                let mut file = OpenOptions::new()
+                    .read(true)
+                    .write(true)
+                    .create(true)
+                    .truncate(false)
+                    .open(path)
+                    .map_err(|e| format!("Opening file: {e}"))?;
+
+                file.lock_exclusive()
+                    .map_err(|e| format!("Locking file: {e}"))?;
+
+                let mut content = String::new();
+                file.read_to_string(&mut content)
+                    .map_err(|e| format!("Reading file: {e}"))?;
+
+                if !write {
+                    file.unlock().map_err(|e| format!("Unlocking file: {e}"))?;
+                }
+
+                (
+                    serde_json::from_str::<GlobalSettings>(&content).map_err(|e| e.to_string())?,
+                    Some(file),
+                )
+            } else {
+                (GlobalSettings::default(), None)
+            };
+
+            update(&mut global_settings);
+
+            if !exists
+                && global_settings.bitbox.is_none()
+                && global_settings.window_config.is_none()
+            {
+                write = false;
+            }
+
+            if write {
+                let mut file = if let Some(file) = file {
+                    file
+                } else {
+                    let file = OpenOptions::new()
+                        .read(true)
+                        .write(true)
+                        .create(true)
+                        .truncate(false)
+                        .open(path)
+                        .map_err(|e| format!("Opening file: {e}"))?;
+
+                    file.lock_exclusive()
+                        .map_err(|e| format!("Locking file: {e}"))?;
+                    file
+                };
+                let content = serde_json::to_vec_pretty(&global_settings)
+                    .map_err(|e| format!("Failed to serialize GlobalSettings: {e}"))?;
+
+                file.seek(SeekFrom::Start(0))
+                    .map_err(|e| format!("Failed to seek file: {e}"))?;
+
+                file.write_all(&content)
+                    .map_err(|e| format!("Failed to write file: {e}"))?;
+                file.set_len(content.len() as u64)
+                    .map_err(|e| format!("Failed to truncate file: {e}"))?;
+                file.unlock().map_err(|e| format!("Unlocking file: {e}"))?;
+            }
+
+            Ok(())
+        }
     }
 
-    #[derive(Debug, Deserialize, Serialize)]
+    #[derive(Debug, Deserialize, Serialize, Clone)]
     pub struct BitboxSettings {
         pub noise_config: NoiseConfigData,
     }
@@ -407,68 +530,209 @@ pub mod global {
         /// in the provided directory.
         pub fn new(global_datadir: &LianaDirectory) -> PersistedBitboxNoiseConfig {
             PersistedBitboxNoiseConfig {
-                file_path: global_datadir.path().join(DEFAULT_FILE_NAME),
+                file_path: GlobalSettings::path(global_datadir),
             }
         }
     }
 
     impl NoiseConfig for PersistedBitboxNoiseConfig {
-        fn read_config(&self) -> Result<NoiseConfigData, async_hwi::bitbox::api::ConfigError> {
-            if !self.file_path.exists() {
-                return Ok(NoiseConfigData::default());
-            }
-
-            let mut file =
-                std::fs::File::open(&self.file_path).map_err(|e| ConfigError(e.to_string()))?;
-
-            let mut contents = String::new();
-            file.read_to_string(&mut contents)
-                .map_err(|e| ConfigError(e.to_string()))?;
-
-            let settings = serde_json::from_str::<Settings>(&contents)
-                .map_err(|e| ConfigError(e.to_string()))?;
-
-            Ok(settings
-                .bitbox
+        fn read_config(&self) -> Result<NoiseConfigData, ConfigError> {
+            let res = GlobalSettings::load_bitbox_settings(&self.file_path)
+                .map_err(ConfigError)?
                 .map(|s| s.noise_config)
-                .unwrap_or_else(NoiseConfigData::default))
+                .unwrap_or_else(NoiseConfigData::default);
+            Ok(res)
         }
 
         fn store_config(&self, conf: &NoiseConfigData) -> Result<(), ConfigError> {
-            let data = if self.file_path.exists() {
-                let mut file =
-                    std::fs::File::open(&self.file_path).map_err(|e| ConfigError(e.to_string()))?;
-
-                let mut contents = String::new();
-                file.read_to_string(&mut contents)
-                    .map_err(|e| ConfigError(e.to_string()))?;
-
-                let mut settings = serde_json::from_str::<Settings>(&contents)
-                    .map_err(|e| ConfigError(e.to_string()))?;
-
-                settings.bitbox = Some(BitboxSettings {
-                    noise_config: conf.clone(),
-                });
-
-                serde_json::to_string_pretty(&settings).map_err(|e| ConfigError(e.to_string()))?
-            } else {
-                serde_json::to_string_pretty(&Settings {
-                    bitbox: Some(BitboxSettings {
-                        noise_config: conf.clone(),
-                    }),
-                })
-                .map_err(|e| ConfigError(e.to_string()))?
-            };
+            GlobalSettings::update(
+                &self.file_path,
+                |s| {
+                    if let Some(bitbox) = s.bitbox.as_mut() {
+                        bitbox.noise_config = conf.clone();
+                    } else {
+                        s.bitbox = Some(BitboxSettings {
+                            noise_config: conf.clone(),
+                        });
+                    }
+                },
+                true,
+            )
+            .map_err(ConfigError)
+        }
+    }
+}
 
-            let mut file = std::fs::OpenOptions::new()
-                .write(true)
-                .create(true)
-                .truncate(true)
-                .open(&self.file_path)
-                .map_err(|e| ConfigError(e.to_string()))?;
+#[cfg(test)]
+mod test {
+    use super::global::{GlobalSettings, WindowConfig};
+    use std::env;
+
+    const RAW_GLOBAL_SETTINGS: &str = r#"{
+          "bitbox": {
+            "noise_config": {
+              "app_static_privkey": [
+                84,
+                118,
+                69,
+                7,
+                5,
+                246,
+                50,
+                252,
+                79,
+                62,
+                233,
+                118,
+                54,
+                46,
+                247,
+                143,
+                255,
+                152,
+                11,
+                96,
+                7,
+                213,
+                209,
+                42,
+                219,
+                58,
+                237,
+                22,
+                53,
+                221,
+                227,
+                228
+              ],
+              "device_static_pubkeys": [
+                [
+                  252,
+                  78,
+                  254,
+                  112,
+                  62,
+                  72,
+                  220,
+                  22,
+                  23,
+                  147,
+                  205,
+                  166,
+                  248,
+                  39,
+                  97,
+                  46,
+                  32,
+                  255,
+                  132,
+                  125,
+                  97,
+                  142,
+                  31,
+                  146,
+                  44,
+                  186,
+                  231,
+                  1,
+                  12,
+                  190,
+                  105,
+                  11
+                ]
+              ]
+            }
+          },
+          "window_config": {
+            "width": 1248.0,
+            "height": 688.0
+          }
+        }"#;
+
+    #[test]
+    fn test_parse_global_config() {
+        let _ = serde_json::from_str::<GlobalSettings>(RAW_GLOBAL_SETTINGS).unwrap();
+    }
 
-            file.write_all(data.as_bytes())
-                .map_err(|e| ConfigError(e.to_string()))
-        }
+    #[test]
+    fn test_update_global_config() {
+        let path = env::current_dir()
+            .unwrap()
+            .join("test_assets")
+            .join("global_settings.json");
+        assert!(path.exists());
+
+        // read global config file
+        GlobalSettings::update(
+            &path,
+            |s| {
+                assert_eq!(
+                    *s.window_config.as_ref().unwrap(),
+                    WindowConfig {
+                        width: 1248.0,
+                        height: 688.0
+                    }
+                );
+                assert!(s.bitbox.is_some());
+                // this must not be written to the file as write == false
+                s.window_config.as_mut().unwrap().height = 0.0;
+            },
+            false,
+        )
+        .unwrap();
+
+        // re-read the global config file
+        GlobalSettings::update(
+            &path,
+            |s| {
+                // change have not been written
+                assert_eq!(
+                    *s.window_config.as_ref().unwrap(),
+                    WindowConfig {
+                        width: 1248.0,
+                        height: 688.0
+                    }
+                );
+            },
+            true,
+        )
+        .unwrap();
+
+        // edit the global config file
+        GlobalSettings::update(
+            &path,
+            |s| {
+                assert_eq!(
+                    *s.window_config.as_ref().unwrap(),
+                    WindowConfig {
+                        width: 1248.0,
+                        height: 688.0
+                    }
+                );
+                assert!(s.bitbox.is_some());
+                // this must be written to the file as write == true
+                s.window_config.as_mut().unwrap().height = 0.0;
+            },
+            true,
+        )
+        .unwrap();
+
+        // re-read the global config file
+        GlobalSettings::update(
+            &path,
+            |s| {
+                // change have been written
+                assert_eq!(
+                    *s.window_config.as_ref().unwrap(),
+                    WindowConfig {
+                        width: 1248.0,
+                        height: 0.0
+                    }
+                );
+                s.window_config.as_mut().unwrap().height = 688.0;
+            },
+            true,
+        )
+        .unwrap()
     }
 }
diff --git a/liana-gui/src/app/state/settings/bitcoind.rs b/liana-gui/src/app/state/settings/bitcoind.rs
index 2fa22be2..c04288f0 100644
--- a/liana-gui/src/app/state/settings/bitcoind.rs
+++ b/liana-gui/src/app/state/settings/bitcoind.rs
@@ -621,7 +621,7 @@ impl RescanSetting {
                     return Task::none();
                 }
                 self.processing = true;
-                info!("Asking daemon to rescan with timestamp: {}", t);
+                info!("Asking deamon to rescan with timestamp: {}", t);
                 return Task::perform(
                     async move {
                         daemon.start_rescan(t.try_into().expect("t cannot be inferior to 0 otherwise genesis block timestamp is chosen"))
diff --git a/liana-gui/src/gui/mod.rs b/liana-gui/src/gui/mod.rs
index d3b4f623..f64ea2dc 100644
--- a/liana-gui/src/gui/mod.rs
+++ b/liana-gui/src/gui/mod.rs
@@ -2,8 +2,9 @@ use iced::{
     event::{self, Event},
     keyboard,
     widget::{focus_next, focus_previous, pane_grid},
-    Length, Subscription, Task,
+    Length, Size, Subscription, Task,
 };
+use iced_runtime::window;
 use tracing::{error, info};
 use tracing_subscriber::filter::LevelFilter;
 extern crate serde;
@@ -15,12 +16,23 @@ use liana_ui::widget::{Column, Container, Element};
 pub mod pane;
 pub mod tab;
 
-use crate::{dir::LianaDirectory, launcher, logger::setup_logger, VERSION};
+use crate::{
+    app::settings::global::{GlobalSettings, WindowConfig},
+    dir::LianaDirectory,
+    launcher,
+    logger::setup_logger,
+    VERSION,
+};
+
+use iced::window::Id;
 
 pub struct GUI {
     panes: pane_grid::State<pane::Pane>,
     focus: Option<pane_grid::Pane>,
     config: Config,
+    window_id: Option<Id>,
+    window_init: Option<bool>,
+    window_config: Option<WindowConfig>,
 }
 
 #[derive(Debug)]
@@ -39,6 +51,8 @@ pub enum Message {
     Clicked(pane_grid::Pane),
     Dragged(pane_grid::DragEvent),
     Resized(pane_grid::ResizeEvent),
+    Window(Option<Id>),
+    WindowSize(Size),
 }
 
 impl From<Result<(), iced::font::Error>> for Message {
@@ -65,15 +79,24 @@ impl GUI {
         if let Err(e) = setup_logger(log_level, config.liana_directory.clone()) {
             tracing::warn!("Error while setting error: {}", e);
         }
-        let mut cmds = vec![Task::perform(ctrl_c(), |_| Message::CtrlC)];
+        let mut cmds = vec![
+            window::get_oldest().map(Message::Window),
+            Task::perform(ctrl_c(), |_| Message::CtrlC),
+        ];
         let (pane, cmd) = pane::Pane::new(&config);
         let (panes, focused_pane) = pane_grid::State::new(pane);
         cmds.push(cmd.map(move |msg| Message::Pane(focused_pane, msg)));
+        let window_config =
+            GlobalSettings::load_window_config(&GlobalSettings::path(&config.liana_directory));
+        let window_init = window_config.is_some().then_some(true);
         (
             Self {
                 panes,
                 focus: Some(focused_pane),
                 config,
+                window_id: None,
+                window_init,
+                window_config,
             },
             Task::batch(cmds),
         )
@@ -81,11 +104,81 @@ impl GUI {
 
     pub fn update(&mut self, message: Message) -> Task<Message> {
         match message {
+            // we get this message only once at startup
+            Message::Window(id) => {
+                self.window_id = id;
+                // Common case: if there is an already saved screen size we reuse it
+                if let (Some(id), Some(WindowConfig { width, height })) = (id, &self.window_config)
+                {
+                    window::resize(
+                        id,
+                        Size {
+                            width: *width,
+                            height: *height,
+                        },
+                    )
+                // Initial startup: we maximize the screen in order to know the max usable screen area
+                } else if let Some(id) = &self.window_id {
+                    window::maximize(*id, true)
+                } else {
+                    Task::none()
+                }
+            }
+            Message::WindowSize(monitor_size) => {
+                let cloned_cfg = self.window_config.clone();
+                match (cloned_cfg, &self.window_init, &self.window_id) {
+                    // no previous screen size recorded && window maximized
+                    (None, Some(false), Some(id)) => {
+                        self.window_init = Some(true);
+                        let mut batch = vec![window::maximize(*id, false)];
+                        let new_size = if monitor_size.height >= 1200.0 {
+                            let size = Size {
+                                width: 1200.0,
+                                height: 950.0,
+                            };
+                            batch.push(window::resize(*id, size));
+                            size
+                        } else {
+                            batch.push(window::resize(*id, iced::window::Settings::default().size));
+                            iced::window::Settings::default().size
+                        };
+                        self.window_config = Some(WindowConfig {
+                            width: new_size.width,
+                            height: new_size.height,
+                        });
+                        Task::batch(batch)
+                    }
+                    // we already have a record of the last window size and we update it
+                    (Some(WindowConfig { width, height }), _, _) => {
+                        if width != monitor_size.width || height != monitor_size.height {
+                            if let Some(cfg) = &mut self.window_config {
+                                cfg.width = monitor_size.width;
+                                cfg.height = monitor_size.height;
+                            }
+                        }
+                        Task::none()
+                    }
+                    // we ignore the first notification about initial window size it will always be
+                    // the default one
+                    _ => {
+                        if self.window_init.is_none() {
+                            self.window_init = Some(false);
+                        }
+                        Task::none()
+                    }
+                }
+            }
             Message::CtrlC
             | Message::Event(iced::Event::Window(iced::window::Event::CloseRequested)) => {
                 for (_, pane) in self.panes.iter_mut() {
                     pane.stop();
                 }
+                if let Some(window_config) = &self.window_config {
+                    let path = GlobalSettings::path(&self.config.liana_directory);
+                    if let Err(e) = GlobalSettings::update_window_config(&path, window_config) {
+                        tracing::error!("Failed to update the window config: {e}");
+                    }
+                }
                 iced::window::get_latest().and_then(iced::window::close)
             }
             Message::KeyPressed(Key::Tab(shift)) => {
@@ -250,6 +343,9 @@ impl GUI {
                     iced::Event::Window(iced::window::Event::CloseRequested),
                     event::Status::Ignored,
                 ) => Some(Message::Event(event)),
+                (iced::Event::Window(iced::window::Event::Resized(size)), _) => {
+                    Some(Message::WindowSize(*size))
+                }
                 _ => None,
             }
         })];
diff --git a/liana-gui/src/installer/view/mod.rs b/liana-gui/src/installer/view/mod.rs
index 37876dcf..93a07270 100644
--- a/liana-gui/src/installer/view/mod.rs
+++ b/liana-gui/src/installer/view/mod.rs
@@ -326,7 +326,7 @@ pub fn import_descriptor<'a>(
     let import_backup = if !valid && !imported_backup {
         Some(
             Row::new()
-                .push(button::primary(None, "Import backup").on_press(Message::ImportBackup))
+                .push(button::secondary(None, "Import backup").on_press(Message::ImportBackup))
                 .push(Space::with_width(Length::Fill)),
         )
     } else {
@@ -336,7 +336,7 @@ pub fn import_descriptor<'a>(
     let backup_imported = if imported_backup {
         Some(
             Row::new()
-                .push(text("Backup successfully imported!").bold())
+                .push(text("Backup successfuly imported!").bold())
                 .push(Space::with_width(Length::Fill)),
         )
     } else {
@@ -351,10 +351,10 @@ pub fn import_descriptor<'a>(
             .push(
                 Column::new()
                     .spacing(20)
+                    .push_maybe(descriptor)
+                    .push_maybe(or)
                     .push_maybe(import_backup)
                     .push_maybe(backup_imported)
-                    .push_maybe(or)
-                    .push_maybe(descriptor)
                     .push(text(
                         "If you are using a Bitcoin Core node, \
                 you will need to perform a rescan of \
diff --git a/liana-gui/src/launcher.rs b/liana-gui/src/launcher.rs
index 1355bf8d..8719cecf 100644
--- a/liana-gui/src/launcher.rs
+++ b/liana-gui/src/launcher.rs
@@ -43,7 +43,7 @@ pub enum State {
 pub struct Launcher {
     state: State,
     network: Network,
-    datadir_path: LianaDirectory,
+    pub datadir_path: LianaDirectory,
     error: Option<String>,
     delete_wallet_modal: Option<DeleteWalletModal>,
 }
diff --git a/liana-gui/src/main.rs b/liana-gui/src/main.rs
index d6f0093c..f95c1e58 100644
--- a/liana-gui/src/main.rs
+++ b/liana-gui/src/main.rs
@@ -14,6 +14,7 @@ use liana::miniscript::bitcoin;
 use liana_ui::{component::text, font, image, theme};
 
 use liana_gui::{
+    app::settings::global::{GlobalSettings, WindowConfig},
     dir::LianaDirectory,
     gui::{Config, GUI},
     node::bitcoind::delete_all_bitcoind_locks_for_process,
@@ -106,8 +107,18 @@ fn main() -> Result<(), Box<dyn Error>> {
         fonts: font::load(),
     };
 
+    let global_config_path = GlobalSettings::path(&config.liana_directory);
+    let initial_size = if let Some(WindowConfig { width, height }) =
+        GlobalSettings::load_window_config(&global_config_path)
+    {
+        Size { width, height }
+    } else {
+        iced::window::Settings::default().size
+    };
+
     #[allow(unused_mut)]
     let mut window_settings = iced::window::Settings {
+        size: initial_size,
         icon: Some(image::liana_app_icon()),
         position: iced::window::Position::Default,
         min_size: Some(Size {
@@ -145,7 +156,7 @@ fn main() -> Result<(), Box<dyn Error>> {
 fn setup_panic_hook(liana_directory: &LianaDirectory) {
     let bitcoind_dir = liana_directory.bitcoind_directory();
     std::panic::set_hook(Box::new(move |panic_info| {
-        error!("Panic occurred");
+        error!("Panic occured");
         if let Err(e) = delete_all_bitcoind_locks_for_process(bitcoind_dir.clone()) {
             error!("Failed to delete internal bitcoind locks: {}", e);
         }
diff --git a/liana-gui/src/services/connect/client/backend/api.rs b/liana-gui/src/services/connect/client/backend/api.rs
index 48581d04..48a37e57 100644
--- a/liana-gui/src/services/connect/client/backend/api.rs
+++ b/liana-gui/src/services/connect/client/backend/api.rs
@@ -257,7 +257,7 @@ pub struct Transaction {
     pub raw: bitcoin::Transaction,
     pub inputs: Vec<Input>,
     pub outputs: Vec<Output>,
-    /// If the transaction has multiple incoming or outgoing payments.
+    /// If the transaction has multiple incoming or ougoing payment.
     pub is_batch: bool,
 }
 


Test patch:
diff --git a/liana-gui/test_assets/global_settings.json b/liana-gui/test_assets/global_settings.json
new file mode 100644
index 00000000..012040ea
--- /dev/null
+++ b/liana-gui/test_assets/global_settings.json
@@ -0,0 +1,80 @@
+{
+  "bitbox": {
+    "noise_config": {
+      "app_static_privkey": [
+        84,
+        118,
+        69,
+        7,
+        5,
+        246,
+        50,
+        252,
+        79,
+        62,
+        233,
+        118,
+        54,
+        46,
+        247,
+        143,
+        255,
+        152,
+        11,
+        96,
+        7,
+        213,
+        209,
+        42,
+        219,
+        58,
+        237,
+        22,
+        53,
+        221,
+        227,
+        228
+      ],
+      "device_static_pubkeys": [
+        [
+          252,
+          78,
+          254,
+          112,
+          62,
+          72,
+          220,
+          22,
+          23,
+          147,
+          205,
+          166,
+          248,
+          39,
+          97,
+          46,
+          32,
+          255,
+          132,
+          125,
+          97,
+          142,
+          31,
+          146,
+          44,
+          186,
+          231,
+          1,
+          12,
+          190,
+          105,
+          11
+        ]
+      ]
+    }
+  },
+  "window_config": {
+    "width": 1248.0,
+    "height": 688.0
+  }
+}