Repository: wizardsardine/liana
Pull request #1198 (2024-07-16 13:59:40)

GUI: Refactor amount formatting
This PR is based on #1182 and aims to actually refactor the amount component while adding a space separator to the integer portion of BTC amount displays as per #962.

The code organization is reworked to try and separate the conversion of `bitcoin::Amount` type to a `iced` renderable `Row` elements in steps via smaller functions by:

1. Converting the `Amount` type to a string with the integer and fraction portions formatted in space-separated three digit chunks.
2. Detecting where in that string the non-zero BTC amount occurs
3. Converting the string into parts and rendering the preceding zeros and spaces with normal styling while applying a bold `Row` render element for the significant amount.

To me, this approach makes the code easier to read and is a small step toward separating the string formatting functionality from the render element building. If we find more bugs with this approach, or the team simply doesn't like the reorganization, it's fine to scrap it, as it was a decent learning exercise anyway. Looking forward to feedback.


Patch:
diff --git a/doc/TRY.md b/doc/TRY.md
index 8e354b77..77fff69c 100644
--- a/doc/TRY.md
+++ b/doc/TRY.md
@@ -7,7 +7,7 @@ This document is a short set of instructions for trying out Liana on Bitcoin sig
 give any nuance, details or describe alternative configurations.
 
 This guide will make use Liana as a "hot wallet", and use the "Liana managed" `bitcoind` option.
-You can find [here](./SIGNING_DEVICES.md) the list of supported signing devices.
+You can also connect your Ledger or SpecterDIY hardware signer if you have some at hand (and maybe by the time you read this guide, Bitbox, Coldcard and Jade - integration is coming soon).
 If you'd like to try out Liana using emulators of
 hardware signing device you can use the [Specter
 simulator](https://github.com/cryptoadvance/specter-diy/blob/master/docs/simulator.md) or the
@@ -69,12 +69,6 @@ gpg --verify liana_2.0-1_amd64.deb.asc
 ```
 GPG should tell you the signature is valid for Antoine's key.
 
-If GPG told you that Antoine key has expired, you should refresh it.
-Example for Linux (replace the signature name with the one corresponding to your download):
-```
-gpg --keyserver hkps://keys.openpgp.org --refresh-keys E13FC145CD3F4304      
-```
-
 If all is good, you can run Liana!
 
 At startup, you will have the choice between starting Liana using an existing configuration or to
diff --git a/doc/USAGE.md b/doc/USAGE.md
index b49f755f..c24e3176 100644
--- a/doc/USAGE.md
+++ b/doc/USAGE.md
@@ -159,10 +159,10 @@ strongly recommend using a hardware signing device for any non-trivial amount.
 
 For now, the following signing devices are supported:
 - Ledger Nano S, S+ & X
-- BitBox02 (P2WSH only)
+- BitBox02
 - Specter DIY
-- Coldcard MK4 & Q
-- Blockstream Jade (P2WSH only)
+- Coldcard MK4
+- Blockstream Jade
 
 For more information (such as compatibility and minimum supported versions) please read the [signing
 devices documentation](./signing_devices.md).
diff --git a/doc/SIGNING_DEVICES.md b/doc/signing_devices.md
similarity index 84%
rename from doc/SIGNING_DEVICES.md
rename to doc/signing_devices.md
index ad41ade9..b6547432 100644
--- a/doc/SIGNING_DEVICES.md
+++ b/doc/signing_devices.md
@@ -10,7 +10,7 @@ repository](https://github.com/wizardsardine/async-hwi).
 
 Version 1.5.0 and above of the firmware is supported for use in P2WSH descriptors.
 
-For use in Taproot descriptors you should use version 1.9.0 or higher.
+Support for use in Taproot descriptors has been implemented but not yet released.
 
 ## [Ledger](https://github.com/LedgerHQ/app-bitcoin-new)
 
@@ -28,8 +28,11 @@ Support for use in Taproot descriptors is not yet available in the firmware.
 Support for use in both P2WSH and Taproot descriptors has only been released in Beta as of this
 writing. It is only supported by the [Edge
 firmware](https://github.com/Coldcard/firmware?tab=readme-ov-file#long-lived-branches).
-For use in Taproot descriptors you should use version 6.3.3 or higher.
 
+As of this writing, Coldcard on Taproot will only be usable for descriptors which only use a single
+key as their primary path. This is due to a discrepancy in how Coldcard derives [deterministically
+unspendable Taproot internal
+keys](https://delvingbitcoin.org/t/unspendable-keys-in-descriptors/304).
 
 ## [Jade](https://github.com/Blockstream/Jade)
 
diff --git a/gui/Cargo.lock b/gui/Cargo.lock
index b5a2133f..fd99df77 100644
--- a/gui/Cargo.lock
+++ b/gui/Cargo.lock
@@ -199,9 +199,9 @@ dependencies = [
 
 [[package]]
 name = "async-hwi"
-version = "0.0.22"
+version = "0.0.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b3f27c4ed452a71a415e1198b2ef0b586ef270f3921b6ebbd558311d46806fd"
+checksum = "a278b99ded5aa103de53c13496b0fd9266f2218ec7a37c8f7083285bba567fa7"
 dependencies = [
  "async-trait",
  "bitbox-api",
@@ -4205,7 +4205,7 @@ version = "0.28.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f622567e3b4b38154fb8190bcf6b160d7a4301d70595a49195b48c116007a27"
 dependencies = [
- "bitcoin_hashes 0.13.0",
+ "bitcoin_hashes 0.12.0",
  "secp256k1-sys",
  "serde",
 ]
diff --git a/gui/Cargo.toml b/gui/Cargo.toml
index 2f0f9c92..92201240 100644
--- a/gui/Cargo.toml
+++ b/gui/Cargo.toml
@@ -15,7 +15,7 @@ path = "src/main.rs"
 
 [dependencies]
 async-trait = "0.1"
-async-hwi = { version = "0.0.22" }
+async-hwi = { version = "0.0.21" }
 liana = { git = "https://github.com/wizardsardine/liana", branch = "master", default-features = false, features = ["nonblocking_shutdown"] }
 liana_ui = { path = "ui" }
 backtrace = "0.3"
@@ -44,7 +44,7 @@ chrono = "0.4.38"
 # Used for managing internal bitcoind
 base64 = "0.21"
 bitcoin_hashes = "0.12"
-reqwest = { version = "0.11", default-features=false, features = ["json", "rustls-tls"] }
+reqwest = { version = "0.11", default-features=false, features = ["rustls-tls"] }
 rust-ini = "0.19.0"
 
 
diff --git a/gui/src/app/error.rs b/gui/src/app/error.rs
index 2ca312d7..b13f8ae7 100644
--- a/gui/src/app/error.rs
+++ b/gui/src/app/error.rs
@@ -29,10 +29,10 @@ impl std::fmt::Display for Error {
                 DaemonError::Unexpected(e) => write!(f, "{}", e),
                 DaemonError::NoAnswer => write!(f, "Daemon did not answer"),
                 DaemonError::DaemonStopped => write!(f, "Daemon stopped"),
-                DaemonError::RpcSocket(Some(ErrorKind::ConnectionRefused), _) => {
+                DaemonError::Transport(Some(ErrorKind::ConnectionRefused), _) => {
                     write!(f, "Failed to connect to daemon")
                 }
-                DaemonError::RpcSocket(kind, e) => {
+                DaemonError::Transport(kind, e) => {
                     if let Some(k) = kind {
                         write!(f, "{} [{:?}]", e, k)
                     } else {
@@ -48,9 +48,6 @@ impl std::fmt::Display for Error {
                 DaemonError::Rpc(code, e) => {
                     write!(f, "[{:?}] {}", code, e)
                 }
-                DaemonError::Http(code, e) => {
-                    write!(f, "[{:?}] {}", code, e)
-                }
                 DaemonError::CoinSelectionError => write!(f, "{}", e),
             },
             Self::Unexpected(e) => write!(f, "Unexpected error: {}", e),
diff --git a/gui/src/app/message.rs b/gui/src/app/message.rs
index 97b77417..9b9de75d 100644
--- a/gui/src/app/message.rs
+++ b/gui/src/app/message.rs
@@ -23,7 +23,8 @@ pub enum Message {
     View(view::Message),
     LoadDaemonConfig(Box<DaemonConfig>),
     DaemonConfigLoaded(Result<(), Error>),
-    LoadWallet(Wallet),
+    LoadWallet,
+    WalletLoaded(Result<Arc<Wallet>, Error>),
     Info(Result<GetInfoResult, Error>),
     ReceiveAddress(Result<(Address, ChildNumber), Error>),
     Coins(Result<Vec<Coin>, Error>),
@@ -33,7 +34,7 @@ pub enum Message {
     RbfPsbt(Result<Txid, Error>),
     Recovery(Result<SpendTx, Error>),
     Signed(Fingerprint, Result<Psbt, Error>),
-    WalletUpdated(Result<Arc<Wallet>, Error>),
+    WalletRegistered(Result<Fingerprint, Error>),
     Updated(Result<(), Error>),
     Saved(Result<(), Error>),
     Verified(Fingerprint, Result<(), Error>),
diff --git a/gui/src/app/mod.rs b/gui/src/app/mod.rs
index cddfb770..4f65b62f 100644
--- a/gui/src/app/mod.rs
+++ b/gui/src/app/mod.rs
@@ -36,7 +36,7 @@ use state::{
 use crate::{
     app::{cache::Cache, error::Error, menu::Menu, wallet::Wallet},
     bitcoind::Bitcoind,
-    daemon::{embedded::EmbeddedDaemon, Daemon, DaemonBackend},
+    daemon::{embedded::EmbeddedDaemon, Daemon},
 };
 
 use self::state::SettingsState;
@@ -58,7 +58,6 @@ impl Panels {
         cache: &Cache,
         wallet: Arc<Wallet>,
         data_dir: PathBuf,
-        daemon_backend: DaemonBackend,
         internal_bitcoind: Option<&Bitcoind>,
     ) -> Panels {
         Self {
@@ -78,7 +77,6 @@ impl Panels {
             settings: state::SettingsState::new(
                 data_dir,
                 wallet.clone(),
-                daemon_backend,
                 internal_bitcoind.is_some(),
             ),
         }
@@ -118,6 +116,7 @@ impl Panels {
 }
 
 pub struct App {
+    data_dir: PathBuf,
     cache: Cache,
     config: Config,
     wallet: Arc<Wallet>,
@@ -139,14 +138,14 @@ impl App {
         let mut panels = Panels::new(
             &cache,
             wallet.clone(),
-            data_dir,
-            daemon.backend(),
+            data_dir.clone(),
             internal_bitcoind.as_ref(),
         );
         let cmd = panels.home.reload(daemon.clone(), wallet.clone());
         (
             Self {
                 panels,
+                data_dir,
                 cache,
                 config,
                 daemon,
@@ -219,26 +218,14 @@ impl App {
 
     pub fn subscription(&self) -> Subscription<Message> {
         Subscription::batch(vec![
-            time::every(Duration::from_secs(
-                // LianaLite has no rescan feature, the cache refresh loop is only
-                // to fetch the new block height tip which is only used to warn user
-                // about recovery availability.
-                if self.daemon.backend() == DaemonBackend::RemoteBackend {
-                    120
-                // For the rescan feature, we set a higher frequency of cache refresh
-                // to give to user an up-to-date view of the rescan progress.
-                } else {
-                    10
-                },
-            ))
-            .map(|_| Message::Tick),
+            time::every(Duration::from_secs(10)).map(|_| Message::Tick),
             self.panels.current().subscription(),
         ])
     }
 
     pub fn stop(&mut self) {
         info!("Close requested");
-        if self.daemon.backend() == DaemonBackend::EmbeddedLianad {
+        if !self.daemon.is_external() {
             if let Err(e) = Handle::current().block_on(async { self.daemon.stop().await }) {
                 error!("{}", e);
             } else {
@@ -258,7 +245,6 @@ impl App {
                 Command::perform(
                     async move {
                         // we check every 10 second if the daemon poller is alive
-                        // or if the access token is not expired.
                         daemon.is_alive().await?;
 
                         let info = daemon.get_info().await?;
@@ -290,13 +276,9 @@ impl App {
                 let res = self.load_daemon_config(&path, *cfg);
                 self.update(Message::DaemonConfigLoaded(res))
             }
-            Message::WalletUpdated(Ok(wallet)) => {
-                self.wallet = wallet.clone();
-                self.panels.current_mut().update(
-                    self.daemon.clone(),
-                    &self.cache,
-                    Message::WalletUpdated(Ok(wallet)),
-                )
+            Message::LoadWallet => {
+                let res = self.load_wallet();
+                self.update(Message::WalletLoaded(res))
             }
             Message::View(view::Message::Menu(menu)) => self.set_current_panel(menu),
             Message::View(view::Message::Clipboard(text)) => clipboard::write(text),
@@ -331,6 +313,15 @@ impl App {
             })
     }
 
+    pub fn load_wallet(&mut self) -> Result<Arc<Wallet>, Error> {
+        let wallet = Wallet::new(self.wallet.main_descriptor.clone())
+            .load_settings(&self.data_dir, self.cache.network)?;
+
+        self.wallet = Arc::new(wallet);
+
+        Ok(self.wallet.clone())
+    }
+
     pub fn view(&self) -> Element<Message> {
         let content = self.panels.current().view(&self.cache).map(Message::View);
         if self.cache.network != bitcoin::Network::Bitcoin {
diff --git a/gui/src/app/state/settings/mod.rs b/gui/src/app/state/settings/mod.rs
index 8d5a4b4b..ff325e27 100644
--- a/gui/src/app/state/settings/mod.rs
+++ b/gui/src/app/state/settings/mod.rs
@@ -14,29 +14,22 @@ use wallet::WalletSettingsState;
 
 use crate::{
     app::{cache::Cache, error::Error, message::Message, state::State, view, wallet::Wallet},
-    daemon::{Daemon, DaemonBackend},
+    daemon::Daemon,
 };
 
 pub struct SettingsState {
     data_dir: PathBuf,
     wallet: Arc<Wallet>,
     setting: Option<Box<dyn State>>,
-    daemon_backend: DaemonBackend,
     internal_bitcoind: bool,
 }
 
 impl SettingsState {
-    pub fn new(
-        data_dir: PathBuf,
-        wallet: Arc<Wallet>,
-        daemon_backend: DaemonBackend,
-        internal_bitcoind: bool,
-    ) -> Self {
+    pub fn new(data_dir: PathBuf, wallet: Arc<Wallet>, internal_bitcoind: bool) -> Self {
         Self {
             data_dir,
             wallet,
             setting: None,
-            daemon_backend,
             internal_bitcoind,
         }
     }
@@ -55,7 +48,7 @@ impl State for SettingsState {
                     BitcoindSettingsState::new(
                         daemon.config().cloned(),
                         cache,
-                        daemon.backend() != DaemonBackend::EmbeddedLianad,
+                        daemon.is_external(),
                         self.internal_bitcoind,
                     )
                     .into(),
@@ -84,13 +77,6 @@ impl State for SettingsState {
                     .map(|s| s.reload(daemon, wallet))
                     .unwrap_or_else(Command::none)
             }
-            Message::WalletUpdated(Ok(wallet)) => {
-                self.wallet = wallet.clone();
-                self.setting
-                    .as_mut()
-                    .map(|s| s.update(daemon, cache, message))
-                    .unwrap_or_else(Command::none)
-            }
             _ => self
                 .setting
                 .as_mut()
@@ -111,7 +97,7 @@ impl State for SettingsState {
         if let Some(setting) = &self.setting {
             setting.view(cache)
         } else {
-            view::settings::list(cache, self.daemon_backend == DaemonBackend::RemoteBackend)
+            view::settings::list(cache)
         }
     }
 
diff --git a/gui/src/app/state/settings/wallet.rs b/gui/src/app/state/settings/wallet.rs
index 34690539..3f2710d2 100644
--- a/gui/src/app/state/settings/wallet.rs
+++ b/gui/src/app/state/settings/wallet.rs
@@ -16,7 +16,7 @@ use crate::{
     app::{
         cache::Cache, error::Error, message::Message, settings, state::State, view, wallet::Wallet,
     },
-    daemon::{Daemon, DaemonBackend},
+    daemon::Daemon,
     hw::{HardwareWallet, HardwareWalletConfig, HardwareWallets},
 };
 
@@ -106,21 +106,30 @@ impl State for WalletSettingsState {
         message: Message,
     ) -> Command<Message> {
         match message {
-            Message::WalletUpdated(res) => {
-                self.processing = false;
-                if let Some(modal) = &mut self.modal {
-                    modal.update(daemon, cache, Message::WalletUpdated(res))
-                } else {
-                    match res {
-                        Ok(wallet) => {
-                            self.keys_aliases = Self::keys_aliases(&wallet);
-                            self.wallet = wallet;
-                            self.updated = true;
-                        }
-                        Err(e) => self.warning = Some(e),
-                    };
+            Message::Updated(res) => match res {
+                Ok(()) => {
+                    self.processing = false;
+                    self.updated = true;
+                    Command::perform(async {}, |_| Message::LoadWallet)
+                }
+                Err(e) => {
+                    self.processing = false;
+                    self.warning = Some(e);
                     Command::none()
                 }
+            },
+            Message::WalletLoaded(res) => {
+                match res {
+                    Ok(wallet) => {
+                        if let Some(modal) = &mut self.modal {
+                            modal.wallet = wallet.clone();
+                        }
+                        self.keys_aliases = Self::keys_aliases(&wallet);
+                        self.wallet = wallet;
+                    }
+                    Err(e) => self.warning = Some(e),
+                };
+                Command::none()
             }
             Message::View(view::Message::Settings(
                 view::SettingsMessage::FingerprintAliasEdited(fg, value),
@@ -147,9 +156,8 @@ impl State for WalletSettingsState {
                             .iter()
                             .map(|(fg, name)| (*fg, name.value.to_owned()))
                             .collect(),
-                        daemon,
                     ),
-                    Message::WalletUpdated,
+                    Message::Updated,
                 )
             }
             Message::View(view::Message::Close) => {
@@ -238,7 +246,7 @@ impl RegisterWalletModal {
 
     fn update(
         &mut self,
-        daemon: Arc<dyn Daemon + Sync + Send>,
+        _daemon: Arc<dyn Daemon + Sync + Send>,
         cache: &Cache,
         message: Message,
     ) -> Command<Message> {
@@ -255,16 +263,13 @@ impl RegisterWalletModal {
                     Command::none()
                 }
             },
-            Message::WalletUpdated(res) => {
+            Message::WalletRegistered(res) => {
                 self.processing = false;
                 self.chosen_hw = None;
                 match res {
-                    Ok(wallet) => {
-                        self.registered = HashSet::new();
-                        for hw in &wallet.hardware_wallets {
-                            self.registered.insert(hw.fingerprint);
-                        }
-                        self.wallet = wallet;
+                    Ok(fingerprint) => {
+                        self.registered.insert(fingerprint);
+                        return Command::perform(async {}, |_| Message::LoadWallet);
                     }
                     Err(e) => {
                         if !matches!(e, Error::HardwareWallet(async_hwi::Error::UserRefused)) {
@@ -290,9 +295,8 @@ impl RegisterWalletModal {
                             device.clone(),
                             *fingerprint,
                             self.wallet.clone(),
-                            daemon,
                         ),
-                        Message::WalletUpdated,
+                        Message::WalletRegistered,
                     )
                 } else {
                     Command::none()
@@ -309,61 +313,40 @@ async fn register_wallet(
     hw: std::sync::Arc<dyn async_hwi::HWI + Send + Sync>,
     fingerprint: Fingerprint,
     wallet: Arc<Wallet>,
-    daemon: Arc<dyn Daemon + Sync + Send>,
-) -> Result<Arc<Wallet>, Error> {
+) -> Result<Fingerprint, Error> {
     let hmac = hw
         .register_wallet(&wallet.name, &wallet.main_descriptor.to_string())
         .await
         .map_err(Error::from)?;
 
     if let Some(hmac) = hmac {
-        let kind = hw.device_kind().to_string();
-        let hw_cfg = HardwareWalletConfig {
-            kind: kind.clone(),
-            token: hex::encode(hmac),
-            fingerprint,
-        };
-
-        if daemon.backend() != DaemonBackend::RemoteBackend {
-            let mut settings = settings::Settings::from_file(data_dir.clone(), network)?;
-            let checksum = wallet.descriptor_checksum();
-
-            if let Some(wallet_setting) = settings
-                .wallets
+        let mut settings = settings::Settings::from_file(data_dir.clone(), network)?;
+        let checksum = wallet.descriptor_checksum();
+        if let Some(wallet_setting) = settings
+            .wallets
+            .iter_mut()
+            .find(|w| w.descriptor_checksum == checksum)
+        {
+            let kind = hw.device_kind().to_string();
+            if let Some(hw_config) = wallet_setting
+                .hardware_wallets
                 .iter_mut()
-                .find(|w| w.descriptor_checksum == checksum)
+                .find(|cfg| cfg.kind == kind && cfg.fingerprint == fingerprint)
             {
-                if let Some(hw_config) = wallet_setting
-                    .hardware_wallets
-                    .iter_mut()
-                    .find(|cfg| cfg.kind == kind && cfg.fingerprint == fingerprint)
-                {
-                    *hw_config = hw_cfg.clone();
-                } else {
-                    wallet_setting.hardware_wallets.push(hw_cfg.clone())
-                }
+                hw_config.token = hex::encode(hmac);
+            } else {
+                wallet_setting.hardware_wallets.push(HardwareWalletConfig {
+                    kind,
+                    token: hex::encode(hmac),
+                    fingerprint,
+                })
             }
-
-            settings.to_file(data_dir, network)?;
         }
 
-        let mut wallet = wallet.as_ref().clone();
-        if let Some(hw_config) = wallet
-            .hardware_wallets
-            .iter_mut()
-            .find(|cfg| cfg.kind == kind && cfg.fingerprint == fingerprint)
-        {
-            *hw_config = hw_cfg.clone();
-        } else {
-            wallet.hardware_wallets.push(hw_cfg)
-        }
-        daemon
-            .update_wallet_metadata(&wallet.keys_aliases, &wallet.hardware_wallets)
-            .await?;
-        return Ok(Arc::new(wallet));
+        settings.to_file(data_dir, network)?;
     }
 
-    Ok(wallet)
+    Ok(fingerprint)
 }
 
 async fn update_keys_aliases(
@@ -371,34 +354,24 @@ async fn update_keys_aliases(
     network: Network,
     wallet: Arc<Wallet>,
     keys_aliases: Vec<(Fingerprint, String)>,
-    daemon: Arc<dyn Daemon + Sync + Send>,
-) -> Result<Arc<Wallet>, Error> {
-    if daemon.backend() != DaemonBackend::RemoteBackend {
-        let mut settings = settings::Settings::from_file(data_dir.clone(), network)?;
-        let checksum = wallet.descriptor_checksum();
-        if let Some(wallet_setting) = settings
-            .wallets
-            .iter_mut()
-            .find(|w| w.descriptor_checksum == checksum)
-        {
-            wallet_setting.keys = keys_aliases
-                .iter()
-                .map(|(master_fingerprint, name)| settings::KeySetting {
-                    master_fingerprint: *master_fingerprint,
-                    name: name.clone(),
-                })
-                .collect();
-        }
-
-        settings.to_file(data_dir, network)?;
+) -> Result<(), Error> {
+    let mut settings = settings::Settings::from_file(data_dir.clone(), network)?;
+    let checksum = wallet.descriptor_checksum();
+    if let Some(wallet_setting) = settings
+        .wallets
+        .iter_mut()
+        .find(|w| w.descriptor_checksum == checksum)
+    {
+        wallet_setting.keys = keys_aliases
+            .into_iter()
+            .map(|(master_fingerprint, name)| settings::KeySetting {
+                master_fingerprint,
+                name,
+            })
+            .collect();
     }
 
-    let mut wallet = wallet.as_ref().clone();
-    wallet.keys_aliases = keys_aliases.into_iter().collect();
-
-    daemon
-        .update_wallet_metadata(&wallet.keys_aliases, &wallet.hardware_wallets)
-        .await?;
+    settings.to_file(data_dir, network)?;
 
-    Ok(Arc::new(wallet))
+    Ok(())
 }
diff --git a/gui/src/app/state/spend/step.rs b/gui/src/app/state/spend/step.rs
index 69f244bb..b9baddac 100644
--- a/gui/src/app/state/spend/step.rs
+++ b/gui/src/app/state/spend/step.rs
@@ -571,7 +571,6 @@ impl Step for DefineSpend {
                     // new coins make more sense to be selected. A redraft is triggered
                     // if all forms are valid (checked in the redraft method)
                     self.redraft(daemon);
-                    self.check_valid();
                 }
                 Err(e) => self.warning = Some(e),
             },
diff --git a/gui/src/app/state/transactions.rs b/gui/src/app/state/transactions.rs
index aa1a62d2..30d5dec4 100644
--- a/gui/src/app/state/transactions.rs
+++ b/gui/src/app/state/transactions.rs
@@ -99,9 +99,7 @@ impl State for TransactionsPanel {
                 Ok(txs) => {
                     self.warning = None;
                     for tx in txs {
-                        if let Some(t) = self.txs.iter_mut().find(|other| other.tx == tx.tx) {
-                            t.labels = tx.labels;
-                        } else {
+                        if !self.txs.iter().any(|other| other.tx == tx.tx) {
                             self.txs.push(tx);
                         }
                     }
diff --git a/gui/src/app/view/psbt.rs b/gui/src/app/view/psbt.rs
index 34b06aee..173769ed 100644
--- a/gui/src/app/view/psbt.rs
+++ b/gui/src/app/view/psbt.rs
@@ -978,14 +978,9 @@ fn change_view(output: &TxOut, network: Network) -> Element<Message> {
     let addr = Address::from_script(&output.script_pubkey, network)
         .unwrap()
         .to_string();
-    Column::new()
+    Row::new()
         .width(Length::Fill)
         .spacing(5)
-        .push(
-            Row::new()
-                .push(Space::with_width(Length::Fill))
-                .push(amount(&output.value)),
-        )
         .push(
             Row::new()
                 .align_items(Alignment::Center)
@@ -1004,6 +999,7 @@ fn change_view(output: &TxOut, network: Network) -> Element<Message> {
                         ),
                 ),
         )
+        .push(amount(&output.value))
         .into()
 }
 
diff --git a/gui/src/app/view/settings.rs b/gui/src/app/view/settings.rs
index 92352f9d..433a6f5a 100644
--- a/gui/src/app/view/settings.rs
+++ b/gui/src/app/view/settings.rs
@@ -32,7 +32,7 @@ use crate::{
     hw::HardwareWallet,
 };
 
-pub fn list(cache: &Cache, is_remote_backend: bool) -> Element<Message> {
+pub fn list(cache: &Cache) -> Element<Message> {
     dashboard(
         &Menu::Settings,
         cache,
@@ -44,27 +44,23 @@ pub fn list(cache: &Cache, is_remote_backend: bool) -> Element<Message> {
                 Button::new(text("Settings").size(30).bold())
                     .style(theme::Button::Transparent)
                     .on_press(Message::Menu(Menu::Settings)))
-            .push_maybe(
-                if !is_remote_backend {
-                    Some(Container::new(
-                        Button::new(
-                            Row::new()
-                                .push(badge::Badge::new(icon::bitcoin_icon()))
-                                .push(text("Bitcoin Core").bold())
-                                .padding(10)
-                                .spacing(20)
-                                .align_items(Alignment::Center)
-                                .width(Length::Fill),
-                        )
-                        .width(Length::Fill)
-                        .style(theme::Button::TransparentBorder)
-                        .on_press(Message::Settings(SettingsMessage::EditBitcoindSettings))
+            .push(
+                Container::new(
+                    Button::new(
+                        Row::new()
+                            .push(badge::Badge::new(icon::bitcoin_icon()))
+                            .push(text("Bitcoin Core").bold())
+                            .padding(10)
+                            .spacing(20)
+                            .align_items(Alignment::Center)
+                            .width(Length::Fill),
                     )
                     .width(Length::Fill)
-                    .style(theme::Container::Card(theme::Card::Simple)))
-                } else {
-                    None
-                }
+                    .style(theme::Button::TransparentBorder)
+                    .on_press(Message::Settings(SettingsMessage::EditBitcoindSettings))
+                )
+                .width(Length::Fill)
+                .style(theme::Container::Card(theme::Card::Simple))
             )
             .push(
                 Container::new(
diff --git a/gui/src/app/view/warning.rs b/gui/src/app/view/warning.rs
index 1496646a..1ec4dfd2 100644
--- a/gui/src/app/view/warning.rs
+++ b/gui/src/app/view/warning.rs
@@ -25,16 +25,12 @@ impl From<&Error> for WarningMessage {
                         WarningMessage("Internal error".to_string())
                     }
                 }
-                DaemonError::Http(Some(code), error) => {
-                    WarningMessage(format!("HTTP error {}: {}", code, error))
-                }
-                DaemonError::Http(None, error) => WarningMessage(format!("HTTP error: {}", error)),
                 DaemonError::Unexpected(_) => WarningMessage("Unknown error".to_string()),
                 DaemonError::Start(_) => WarningMessage("Daemon failed to start".to_string()),
                 DaemonError::ClientNotSupported => {
                     WarningMessage("Daemon client is not supported".to_string())
                 }
-                DaemonError::NoAnswer | DaemonError::RpcSocket(..) => {
+                DaemonError::NoAnswer | DaemonError::Transport(..) => {
                     WarningMessage("Communication with Daemon failed".to_string())
                 }
                 DaemonError::DaemonStopped => WarningMessage("Daemon stopped".to_string()),
diff --git a/gui/src/app/wallet.rs b/gui/src/app/wallet.rs
index 2fb83226..d6b4405d 100644
--- a/gui/src/app/wallet.rs
+++ b/gui/src/app/wallet.rs
@@ -1,6 +1,5 @@
 use std::collections::{HashMap, HashSet};
 use std::path::Path;
-use std::sync::Arc;
 
 use crate::{app::settings, hw::HardwareWalletConfig, signer::Signer};
 
@@ -25,13 +24,13 @@ pub fn wallet_name(main_descriptor: &LianaDescriptor) -> String {
     )
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug)]
 pub struct Wallet {
     pub name: String,
     pub main_descriptor: LianaDescriptor,
     pub keys_aliases: HashMap<Fingerprint, String>,
     pub hardware_wallets: Vec<HardwareWalletConfig>,
-    pub signer: Option<Arc<Signer>>,
+    pub signer: Option<Signer>,
 }
 
 impl Wallet {
@@ -61,7 +60,7 @@ impl Wallet {
     }
 
     pub fn with_signer(mut self, signer: Signer) -> Self {
-        self.signer = Some(Arc::new(signer));
+        self.signer = Some(signer);
         self
     }
 
@@ -88,12 +87,12 @@ impl Wallet {
             .to_string()
     }
 
-    pub fn load_from_settings(
+    pub fn load_settings(
         self,
         datadir_path: &Path,
         network: bitcoin::Network,
     ) -> Result<Self, WalletError> {
-        let wallet = match settings::Settings::from_file(datadir_path.to_path_buf(), network) {
+        let mut wallet = match settings::Settings::from_file(datadir_path.to_path_buf(), network) {
             Ok(settings) => {
                 if let Some(wallet_setting) = settings.wallets.first() {
                     self.with_name(wallet_setting.name.clone())
@@ -115,14 +114,6 @@ impl Wallet {
             Err(e) => return Err(e.into()),
         };
 
-        Ok(wallet)
-    }
-
-    pub fn load_hotsigners(
-        self,
-        datadir_path: &Path,
-        network: bitcoin::Network,
-    ) -> Result<Self, WalletError> {
         let hot_signers = match HotSigner::from_datadir(datadir_path, network) {
             Ok(signers) => signers,
             Err(e) => match e {
@@ -138,15 +129,15 @@ impl Wallet {
         };
 
         let curve = bitcoin::secp256k1::Secp256k1::signing_only();
-        let keys = self.descriptor_keys();
+        let keys = wallet.descriptor_keys();
         if let Some(hot_signer) = hot_signers
             .into_iter()
             .find(|s| keys.contains(&s.fingerprint(&curve)))
         {
-            Ok(self.with_signer(Signer::new(hot_signer)))
-        } else {
-            Ok(self)
+            wallet = wallet.with_signer(Signer::new(hot_signer));
         }
+
+        Ok(wallet)
     }
 }
 
diff --git a/gui/src/bitcoind.rs b/gui/src/bitcoind.rs
index 07a8bf8b..c0308a0d 100644
--- a/gui/src/bitcoind.rs
+++ b/gui/src/bitcoind.rs
@@ -22,22 +22,22 @@ use std::os::windows::process::CommandExt;
 const CREATE_NO_WINDOW: u32 = 0x08000000;
 
 /// Current and previous managed bitcoind versions, in order of descending version.
-pub const VERSIONS: [&str; 5] = ["27.1", "26.1", "26.0", "25.1", "25.0"];
+pub const VERSIONS: [&str; 4] = ["26.1", "26.0", "25.1", "25.0"];
 
 /// Current managed bitcoind version for new installations.
 pub const VERSION: &str = VERSIONS[0];
 
 #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
-pub const SHA256SUM: &str = "6d94bde5541a18964c1c36b0f12334004e45e195f244e381fd459827b1fdc395";
+pub const SHA256SUM: &str = "acb50edd20692a9d023de12da573b64ca0fd9b4e9a2b88d1251020a3022b0f27";
 
 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
-pub const SHA256SUM: &str = "ad4a3fd484077224a82dd56d194efb6e614467f413ab1dfb8776da4d08a4c227";
+pub const SHA256SUM: &str = "8a8e415763b7ffd5988153cf03967d812eca629016dd3b0ddf6da3ab6f4a3621";
 
 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
-pub const SHA256SUM: &str = "c9840607d230d65f6938b81deaec0b98fe9cb14c3a41a5b13b2c05d044a48422";
+pub const SHA256SUM: &str = "a5b7d206384a8100058d3f2e2f02123a8e49e83f523499e70e86e121a4897d5b";
 
 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
-pub const SHA256SUM: &str = "9719871a2c9a45c741e33d670d2319dcd3f8f52a6059e9c435a9a2841188b932";
+pub const SHA256SUM: &str = "7bd0849e47472aeff99a0ea2c0cefd98f5be829e5a2d3b0168b5a54456cc638a";
 
 #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
 pub fn download_filename() -> String {
diff --git a/gui/src/daemon/client/jsonrpc.rs b/gui/src/daemon/client/jsonrpc.rs
index 3eeb12dd..1d9fd302 100644
--- a/gui/src/daemon/client/jsonrpc.rs
+++ b/gui/src/daemon/client/jsonrpc.rs
@@ -240,13 +240,13 @@ impl error::Error for Error {
 impl From<Error> for super::DaemonError {
     fn from(e: Error) -> super::DaemonError {
         match e {
-            Error::Io(e) => super::DaemonError::RpcSocket(Some(e.kind()), format!("io: {:?}", e)),
-            Error::Json(e) => super::DaemonError::RpcSocket(None, format!("json decode: {}", e)),
+            Error::Io(e) => super::DaemonError::Transport(Some(e.kind()), format!("io: {:?}", e)),
+            Error::Json(e) => super::DaemonError::Transport(None, format!("json decode: {}", e)),
             Error::NonceMismatch => {
-                super::DaemonError::RpcSocket(None, format!("transport: {}", e))
+                super::DaemonError::Transport(None, format!("transport: {}", e))
             }
             Error::VersionMismatch => {
-                super::DaemonError::RpcSocket(None, format!("transport: {}", e))
+                super::DaemonError::Transport(None, format!("transport: {}", e))
             }
             Error::NoErrorOrResult => super::DaemonError::NoAnswer,
             Error::NotSupported => super::DaemonError::ClientNotSupported,
diff --git a/gui/src/daemon/client/mod.rs b/gui/src/daemon/client/mod.rs
index 8f546f18..57cd637b 100644
--- a/gui/src/daemon/client/mod.rs
+++ b/gui/src/daemon/client/mod.rs
@@ -18,7 +18,7 @@ use liana::{
     miniscript::bitcoin::{address, psbt::Psbt, Address, OutPoint, Txid},
 };
 
-use super::{model::*, Daemon, DaemonBackend, DaemonError};
+use super::{model::*, Daemon, DaemonError};
 
 pub trait Client {
     type Error: Into<DaemonError> + Debug;
@@ -55,8 +55,8 @@ impl<C: Client> Lianad<C> {
 
 #[async_trait]
 impl<C: Client + Send + Sync + Debug> Daemon for Lianad<C> {
-    fn backend(&self) -> DaemonBackend {
-        DaemonBackend::ExternalLianad
+    fn is_external(&self) -> bool {
+        true
     }
 
     fn config(&self) -> Option<&Config> {
diff --git a/gui/src/daemon/embedded.rs b/gui/src/daemon/embedded.rs
index f9ee842c..98e63037 100644
--- a/gui/src/daemon/embedded.rs
+++ b/gui/src/daemon/embedded.rs
@@ -1,7 +1,7 @@
 use std::collections::{HashMap, HashSet};
 use tokio::sync::Mutex;
 
-use super::{model::*, Daemon, DaemonBackend, DaemonError};
+use super::{model::*, Daemon, DaemonError};
 use async_trait::async_trait;
 use liana::{
     commands::{CoinStatus, LabelItem},
@@ -49,8 +49,8 @@ impl std::fmt::Debug for EmbeddedDaemon {
 
 #[async_trait]
 impl Daemon for EmbeddedDaemon {
-    fn backend(&self) -> DaemonBackend {
-        DaemonBackend::EmbeddedLianad
+    fn is_external(&self) -> bool {
+        false
     }
 
     fn config(&self) -> Option<&Config> {
diff --git a/gui/src/daemon/mod.rs b/gui/src/daemon/mod.rs
index 43398ce6..0a462a6c 100644
--- a/gui/src/daemon/mod.rs
+++ b/gui/src/daemon/mod.rs
@@ -13,22 +13,16 @@ use async_trait::async_trait;
 use liana::{
     commands::{CoinStatus, LabelItem, TransactionInfo},
     config::Config,
-    miniscript::bitcoin::{
-        address, bip32::Fingerprint, psbt::Psbt, secp256k1, Address, OutPoint, Txid,
-    },
+    miniscript::bitcoin::{address, psbt::Psbt, secp256k1, Address, OutPoint, Txid},
     StartupError,
 };
 
-use crate::hw::HardwareWalletConfig;
-
 #[derive(Debug)]
 pub enum DaemonError {
     /// Something was wrong with the request.
     Rpc(i32, String),
-    /// Something was wrong with the rpc socket communication.
-    RpcSocket(Option<ErrorKind>, String),
-    /// Something was wrong with the http communication.
-    Http(Option<u16>, String),
+    /// Something was wrong with the communication.
+    Transport(Option<ErrorKind>, String),
     /// Something unexpected happened.
     Unexpected(String),
     /// No response.
@@ -49,8 +43,7 @@ impl std::fmt::Display for DaemonError {
             Self::Rpc(code, e) => write!(f, "Daemon error rpc call: [{:?}] {}", code, e),
             Self::NoAnswer => write!(f, "Daemon returned no answer"),
             Self::DaemonStopped => write!(f, "Daemon stopped"),
-            Self::RpcSocket(kind, e) => write!(f, "Daemon transport error: [{:?}] {}", kind, e),
-            Self::Http(kind, e) => write!(f, "Http error: [{:?}] {}", kind, e),
+            Self::Transport(kind, e) => write!(f, "Daemon transport error: [{:?}] {}", kind, e),
             Self::Unexpected(e) => write!(f, "Daemon unexpected error: {}", e),
             Self::Start(e) => write!(f, "Daemon did not start: {}", e),
             Self::ClientNotSupported => write!(f, "Daemon communication is not supported"),
@@ -59,16 +52,9 @@ impl std::fmt::Display for DaemonError {
     }
 }
 
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum DaemonBackend {
-    EmbeddedLianad,
-    ExternalLianad,
-    RemoteBackend,
-}
-
 #[async_trait]
 pub trait Daemon: Debug {
-    fn backend(&self) -> DaemonBackend;
+    fn is_external(&self) -> bool;
     fn config(&self) -> Option<&Config>;
     async fn is_alive(&self) -> Result<(), DaemonError>;
     async fn stop(&self) -> Result<(), DaemonError>;
@@ -277,10 +263,6 @@ pub trait Daemon: Debug {
             }
         }
 
-        if txids.is_empty() {
-            return Ok(Vec::new());
-        }
-
         let txs = self.list_txs(&txids).await?.transactions;
         let mut txs = txs
             .into_iter()
@@ -313,14 +295,6 @@ pub trait Daemon: Debug {
         load_labels(self, &mut txs).await?;
         Ok(txs)
     }
-    /// Implemented by LianaLite backend
-    async fn update_wallet_metadata(
-        &self,
-        _fingerprint_aliases: &HashMap<Fingerprint, String>,
-        _hws: &[HardwareWalletConfig],
-    ) -> Result<(), DaemonError> {
-        Ok(())
-    }
 }
 
 async fn load_labels<T: model::Labelled, D: Daemon + ?Sized>(
diff --git a/gui/src/hw.rs b/gui/src/hw.rs
index 0b3d32b6..51126270 100644
--- a/gui/src/hw.rs
+++ b/gui/src/hw.rs
@@ -843,28 +843,15 @@ fn ledger_version_supported(version: Option<&Version>) -> bool {
 
 // Kind and minimal version of devices supporting tapminiscript.
 // We cannot use a lazy_static HashMap yet, because DeviceKind does not implement Hash.
-const DEVICES_COMPATIBLE_WITH_TAPMINISCRIPT: [(DeviceKind, Option<Version>); 4] = [
-    (
-        DeviceKind::Ledger,
-        Some(Version {
-            major: 2,
-            minor: 2,
-            patch: 0,
-            prerelease: None,
-        }),
-    ),
-    (DeviceKind::Specter, None),
-    (DeviceKind::SpecterSimulator, None),
-    (
-        DeviceKind::Coldcard,
-        Some(Version {
-            major: 6,
-            minor: 3,
-            patch: 3,
-            prerelease: None,
-        }),
-    ),
-];
+const DEVICES_COMPATIBLE_WITH_TAPMINISCRIPT: [(DeviceKind, Option<Version>); 1] = [(
+    DeviceKind::Ledger,
+    Some(Version {
+        major: 2,
+        minor: 2,
+        patch: 0,
+        prerelease: None,
+    }),
+)];
 
 pub fn is_compatible_with_tapminiscript(
     device_kind: &DeviceKind,
diff --git a/gui/src/installer/message.rs b/gui/src/installer/message.rs
index 5c2434d7..a0be0322 100644
--- a/gui/src/installer/message.rs
+++ b/gui/src/installer/message.rs
@@ -12,7 +12,7 @@ use async_hwi::{DeviceKind, Version};
 #[derive(Debug, Clone)]
 pub enum Message {
     CreateWallet,
-    ShareXpubs,
+    ParticipateWallet,
     ImportWallet,
     UserActionDone(bool),
     Exit(PathBuf, Option<Bitcoind>),
diff --git a/gui/src/installer/mod.rs b/gui/src/installer/mod.rs
index 89bc6109..29fd1cc4 100644
--- a/gui/src/installer/mod.rs
+++ b/gui/src/installer/mod.rs
@@ -27,8 +27,8 @@ use crate::{
 pub use message::Message;
 use step::{
     BackupDescriptor, BackupMnemonic, DefineBitcoind, DefineDescriptor, Final, ImportDescriptor,
-    InternalBitcoindStep, RecoverMnemonic, RegisterDescriptor, SelectBitcoindTypeStep, ShareXpubs,
-    Step, Welcome,
+    InternalBitcoindStep, ParticipateXpub, RecoverMnemonic, RegisterDescriptor,
+    SelectBitcoindTypeStep, Step, Welcome,
 };
 
 pub struct Installer {
@@ -155,10 +155,17 @@ impl Installer {
                 ];
                 self.next()
             }
-            Message::ShareXpubs => {
+            Message::ParticipateWallet => {
                 self.steps = vec![
                     Welcome::default().into(),
-                    ShareXpubs::new(self.network, self.signer.clone()).into(),
+                    ParticipateXpub::new(self.network, self.signer.clone()).into(),
+                    ImportDescriptor::new(self.network).into(),
+                    BackupMnemonic::new(self.signer.clone()).into(),
+                    RegisterDescriptor::new_import_wallet().into(),
+                    SelectBitcoindTypeStep::new().into(),
+                    InternalBitcoindStep::new(&self.context.data_dir).into(),
+                    DefineBitcoind::new().into(),
+                    Final::new().into(),
                 ];
                 self.next()
             }
diff --git a/gui/src/installer/step/descriptor.rs b/gui/src/installer/step/descriptor.rs
index 3918d608..caa1613b 100644
--- a/gui/src/installer/step/descriptor.rs
+++ b/gui/src/installer/step/descriptor.rs
@@ -9,7 +9,7 @@ use liana::{
     descriptors::{LianaDescriptor, LianaPolicy, PathInfo},
     miniscript::{
         bitcoin::{
-            bip32::{DerivationPath, Fingerprint},
+            bip32::{ChildNumber, DerivationPath, Fingerprint},
             Network,
         },
         descriptor::{
@@ -1064,7 +1064,7 @@ impl DescriptorEditModal for EditXpubModal {
     }
 }
 
-pub fn default_derivation_path(network: Network) -> DerivationPath {
+fn default_derivation_path(network: Network) -> DerivationPath {
     DerivationPath::from_str({
         if network == Network::Bitcoin {
             "m/48'/0'/0'/2'"
@@ -1077,7 +1077,7 @@ pub fn default_derivation_path(network: Network) -> DerivationPath {
 
 /// LIANA_STANDARD_PATH: m/48'/0'/0'/2';
 /// LIANA_TESTNET_STANDARD_PATH: m/48'/1'/0'/2';
-pub async fn get_extended_pubkey(
+async fn get_extended_pubkey(
     hw: std::sync::Arc<dyn async_hwi::HWI + Send + Sync>,
     fingerprint: Fingerprint,
     network: Network,
@@ -1095,6 +1095,178 @@ pub async fn get_extended_pubkey(
     }))
 }
 
+pub struct HardwareWalletXpubs {
+    fingerprint: Fingerprint,
+    xpubs: Vec<String>,
+    processing: bool,
+    error: Option<Error>,
+}
+
+pub struct SignerXpubs {
+    signer: Arc<Mutex<Signer>>,
+    xpubs: Vec<String>,
+    next_account: ChildNumber,
+}
+
+impl SignerXpubs {
+    fn new(signer: Arc<Mutex<Signer>>) -> Self {
+        Self {
+            signer,
+            xpubs: Vec::new(),
+            next_account: ChildNumber::from_hardened_idx(0).unwrap(),
+        }
+    }
+
+    fn select(&mut self, network: Network) {
+        self.next_account = self.next_account.increment().unwrap();
+        let signer = self.signer.lock().unwrap();
+        let derivation_path = default_derivation_path(network);
+        // We keep only one for the moment.
+        self.xpubs = vec![format!(
+            "[{}{}]{}",
+            signer.fingerprint(),
+            derivation_path.to_string().trim_start_matches('m'),
+            signer.get_extended_pubkey(&derivation_path)
+        )];
+    }
+
+    pub fn view(&self) -> Element<Message> {
+        view::signer_xpubs(&self.xpubs)
+    }
+}
+
+pub struct ParticipateXpub {
+    network: Network,
+
+    shared: bool,
+
+    hw_xpubs: Vec<HardwareWalletXpubs>,
+    xpubs_signer: SignerXpubs,
+}
+
+impl ParticipateXpub {
+    pub fn new(network: Network, signer: Arc<Mutex<Signer>>) -> Self {
+        Self {
+            network,
+            hw_xpubs: Vec::new(),
+            shared: false,
+            xpubs_signer: SignerXpubs::new(signer),
+        }
+    }
+}
+
+impl Step for ParticipateXpub {
+    // form value is set as valid each time it is edited.
+    // Verification of the values is happening when the user click on Next button.
+    fn update(&mut self, hws: &mut HardwareWallets, message: Message) -> Command<Message> {
+        match message {
+            Message::UserActionDone(shared) => self.shared = shared,
+            Message::ImportXpub(fg, res) => {
+                if let Some(hw_xpubs) = self.hw_xpubs.iter_mut().find(|x| x.fingerprint == fg) {
+                    hw_xpubs.processing = false;
+                    match res {
+                        Err(e) => {
+                            hw_xpubs.error = e.into();
+                        }
+                        Ok(xpub) => {
+                            hw_xpubs.error = None;
+                            // We keep only one for the moment.
+                            hw_xpubs.xpubs = vec![xpub.to_string()];
+                        }
+                    }
+                }
+            }
+            Message::UseHotSigner => {
+                self.xpubs_signer.select(self.network);
+            }
+            Message::Select(i) => {
+                if let Some(HardwareWallet::Supported {
+                    device,
+                    fingerprint,
+                    ..
+                }) = hws.list.get(i)
+                {
+                    let device = device.clone();
+                    let fingerprint = *fingerprint;
+                    let network = self.network;
+                    if let Some(hw_xpubs) = self
+                        .hw_xpubs
+                        .iter_mut()
+                        .find(|x| x.fingerprint == fingerprint)
+                    {
+                        hw_xpubs.processing = true;
+                        hw_xpubs.error = None;
+                    } else {
+                        self.hw_xpubs.push(HardwareWalletXpubs {
+                            fingerprint,
+                            xpubs: Vec::new(),
+                            processing: true,
+                            error: None,
+                        });
+                    }
+                    return Command::perform(
+                        async move {
+                            (
+                                fingerprint,
+                                get_extended_pubkey(device, fingerprint, network).await,
+                            )
+                        },
+                        |(fingerprint, res)| Message::ImportXpub(fingerprint, res),
+                    );
+                }
+            }
+            _ => {}
+        };
+        Command::none()
+    }
+
+    fn subscription(&self, hws: &HardwareWallets) -> Subscription<Message> {
+        hws.refresh().map(Message::HardwareWallets)
+    }
+
+    fn apply(&mut self, ctx: &mut Context) -> bool {
+        ctx.bitcoin_config.network = self.network;
+        // Drop connections to hardware wallets.
+        self.hw_xpubs = Vec::new();
+        true
+    }
+
+    fn view<'a>(&'a self, hws: &'a HardwareWallets, progress: (usize, usize)) -> Element<Message> {
+        view::participate_xpub(
+            progress,
+            hws.list
+                .iter()
+                .enumerate()
+                .map(|(i, hw)| {
+                    if let Some(hw_xpubs) = self
+                        .hw_xpubs
+                        .iter()
+                        .find(|h| hw.fingerprint() == Some(h.fingerprint))
+                    {
+                        view::hardware_wallet_xpubs(
+                            i,
+                            hw,
+                            Some(&hw_xpubs.xpubs),
+                            hw_xpubs.processing,
+                            hw_xpubs.error.as_ref(),
+                        )
+                    } else {
+                        view::hardware_wallet_xpubs(i, hw, None, false, None)
+                    }
+                })
+                .collect(),
+            self.xpubs_signer.view(),
+            self.shared,
+        )
+    }
+}
+
+impl From<ParticipateXpub> for Box<dyn Step> {
+    fn from(s: ParticipateXpub) -> Box<dyn Step> {
+        Box::new(s)
+    }
+}
+
 pub struct ImportDescriptor {
     network: Network,
     imported_descriptor: form::Value<String>,
@@ -1222,11 +1394,6 @@ impl RegisterDescriptor {
 
 impl Step for RegisterDescriptor {
     fn load_context(&mut self, ctx: &Context) {
-        // we reset device registered set if the descriptor have changed.
-        if self.descriptor != ctx.descriptor {
-            self.registered = Default::default();
-            self.done = false;
-        }
         self.descriptor.clone_from(&ctx.descriptor);
         let mut map = HashMap::new();
         for key in ctx.keys.iter().filter(|k| !k.name.is_empty()) {
diff --git a/gui/src/installer/step/mod.rs b/gui/src/installer/step/mod.rs
index ed92782d..34024a20 100644
--- a/gui/src/installer/step/mod.rs
+++ b/gui/src/installer/step/mod.rs
@@ -1,16 +1,16 @@
 mod bitcoind;
 mod descriptor;
 mod mnemonic;
-mod share_xpubs;
 
 pub use bitcoind::{
     DefineBitcoind, DownloadState, InstallState, InternalBitcoindStep, SelectBitcoindTypeStep,
 };
 
-pub use descriptor::{BackupDescriptor, DefineDescriptor, ImportDescriptor, RegisterDescriptor};
+pub use descriptor::{
+    BackupDescriptor, DefineDescriptor, ImportDescriptor, ParticipateXpub, RegisterDescriptor,
+};
 
 pub use mnemonic::{BackupMnemonic, RecoverMnemonic};
-pub use share_xpubs::ShareXpubs;
 
 use std::path::PathBuf;
 
diff --git a/gui/src/installer/step/share_xpubs.rs b/gui/src/installer/step/share_xpubs.rs
deleted file mode 100644
index 40dd1f5c..00000000
--- a/gui/src/installer/step/share_xpubs.rs
+++ /dev/null
@@ -1,195 +0,0 @@
-use std::sync::{Arc, Mutex};
-
-use iced::{Command, Subscription};
-use liana::miniscript::bitcoin::{
-    bip32::{ChildNumber, Fingerprint},
-    Network,
-};
-
-use liana_ui::widget::Element;
-
-use crate::{
-    hw::{HardwareWallet, HardwareWallets},
-    installer::{
-        message::Message,
-        step::{
-            descriptor::{default_derivation_path, get_extended_pubkey},
-            Context, Step,
-        },
-        view, Error,
-    },
-    signer::Signer,
-};
-
-pub struct HardwareWalletXpubs {
-    fingerprint: Fingerprint,
-    xpubs: Vec<String>,
-    processing: bool,
-    error: Option<Error>,
-}
-
-pub struct SignerXpubs {
-    signer: Arc<Mutex<Signer>>,
-    xpubs: Vec<String>,
-    next_account: ChildNumber,
-    words: [&'static str; 12],
-    did_backup: bool,
-}
-
-impl SignerXpubs {
-    fn new(signer: Arc<Mutex<Signer>>) -> Self {
-        let words = { signer.lock().unwrap().mnemonic() };
-        Self {
-            words,
-            signer,
-            xpubs: Vec::new(),
-            next_account: ChildNumber::from_hardened_idx(0).unwrap(),
-            did_backup: false,
-        }
-    }
-
-    fn select(&mut self, network: Network) {
-        self.next_account = self.next_account.increment().unwrap();
-        let signer = self.signer.lock().unwrap();
-        let derivation_path = default_derivation_path(network);
-        // We keep only one for the moment.
-        self.xpubs = vec![format!(
-            "[{}{}]{}",
-            signer.fingerprint(),
-            derivation_path.to_string().trim_start_matches('m'),
-            signer.get_extended_pubkey(&derivation_path)
-        )];
-    }
-
-    pub fn view(&self) -> Element<Message> {
-        view::signer_xpubs(&self.xpubs, &self.words, self.did_backup)
-    }
-}
-
-pub struct ShareXpubs {
-    network: Network,
-    hw_xpubs: Vec<HardwareWalletXpubs>,
-    xpubs_signer: SignerXpubs,
-}
-
-impl ShareXpubs {
-    pub fn new(network: Network, signer: Arc<Mutex<Signer>>) -> Self {
-        Self {
-            network,
-            hw_xpubs: Vec::new(),
-            xpubs_signer: SignerXpubs::new(signer),
-        }
-    }
-}
-
-impl Step for ShareXpubs {
-    // form value is set as valid each time it is edited.
-    // Verification of the values is happening when the user click on Next button.
-    fn update(&mut self, hws: &mut HardwareWallets, message: Message) -> Command<Message> {
-        match message {
-            Message::ImportXpub(fg, res) => {
-                if let Some(hw_xpubs) = self.hw_xpubs.iter_mut().find(|x| x.fingerprint == fg) {
-                    hw_xpubs.processing = false;
-                    match res {
-                        Err(e) => {
-                            hw_xpubs.error = e.into();
-                        }
-                        Ok(xpub) => {
-                            hw_xpubs.error = None;
-                            // We keep only one for the moment.
-                            hw_xpubs.xpubs = vec![xpub.to_string()];
-                        }
-                    }
-                }
-            }
-            Message::UseHotSigner => {
-                self.xpubs_signer.select(self.network);
-            }
-            Message::UserActionDone(done) => {
-                self.xpubs_signer.did_backup = done;
-            }
-            Message::Select(i) => {
-                if let Some(HardwareWallet::Supported {
-                    device,
-                    fingerprint,
-                    ..
-                }) = hws.list.get(i)
-                {
-                    let device = device.clone();
-                    let fingerprint = *fingerprint;
-                    let network = self.network;
-                    if let Some(hw_xpubs) = self
-                        .hw_xpubs
-                        .iter_mut()
-                        .find(|x| x.fingerprint == fingerprint)
-                    {
-                        hw_xpubs.processing = true;
-                        hw_xpubs.error = None;
-                    } else {
-                        self.hw_xpubs.push(HardwareWalletXpubs {
-                            fingerprint,
-                            xpubs: Vec::new(),
-                            processing: true,
-                            error: None,
-                        });
-                    }
-                    return Command::perform(
-                        async move {
-                            (
-                                fingerprint,
-                                get_extended_pubkey(device, fingerprint, network).await,
-                            )
-                        },
-                        |(fingerprint, res)| Message::ImportXpub(fingerprint, res),
-                    );
-                }
-            }
-            _ => {}
-        };
-        Command::none()
-    }
-
-    fn subscription(&self, hws: &HardwareWallets) -> Subscription<Message> {
-        hws.refresh().map(Message::HardwareWallets)
-    }
-
-    fn apply(&mut self, ctx: &mut Context) -> bool {
-        ctx.bitcoin_config.network = self.network;
-        // Drop connections to hardware wallets.
-        self.hw_xpubs = Vec::new();
-        true
-    }
-
-    fn view<'a>(&'a self, hws: &'a HardwareWallets, _progress: (usize, usize)) -> Element<Message> {
-        view::share_xpubs(
-            hws.list
-                .iter()
-                .enumerate()
-                .map(|(i, hw)| {
-                    if let Some(hw_xpubs) = self
-                        .hw_xpubs
-                        .iter()
-                        .find(|h| hw.fingerprint() == Some(h.fingerprint))
-                    {
-                        view::hardware_wallet_xpubs(
-                            i,
-                            hw,
-                            Some(&hw_xpubs.xpubs),
-                            hw_xpubs.processing,
-                            hw_xpubs.error.as_ref(),
-                        )
-                    } else {
-                        view::hardware_wallet_xpubs(i, hw, None, false, None)
-                    }
-                })
-                .collect(),
-            self.xpubs_signer.view(),
-        )
-    }
-}
-
-impl From<ShareXpubs> for Box<dyn Step> {
-    fn from(s: ShareXpubs) -> Box<dyn Step> {
-        Box::new(s)
-    }
-}
diff --git a/gui/src/installer/view.rs b/gui/src/installer/view.rs
index e94285fb..3f160c32 100644
--- a/gui/src/installer/view.rs
+++ b/gui/src/installer/view.rs
@@ -38,32 +38,7 @@ pub fn welcome<'a>() -> Element<'a, Message> {
     Container::new(
         Column::new()
             .push(
-                Container::new(
-                    Row::new()
-                        .align_items(Alignment::Center)
-                        .push(
-                            Container::new(image::liana_brand_grey().width(Length::Fixed(200.0)))
-                                .width(Length::Fill),
-                        )
-                        .push(
-                            Row::new()
-                                .push(
-                                    button::secondary(
-                                        Some(icon::previous_icon()),
-                                        "Change network",
-                                    )
-                                    .width(Length::Fixed(200.0))
-                                    .on_press(Message::BackToLauncher),
-                                )
-                                .push(
-                                    button::secondary(None, "Share Xpubs")
-                                        .width(Length::Fixed(200.0))
-                                        .on_press(Message::ShareXpubs),
-                                )
-                                .spacing(20),
-                        ),
-                )
-                .padding(100),
+                Container::new(image::liana_brand_grey().width(Length::Fixed(200.0))).padding(100),
             )
             .push(
                 Container::new(
@@ -94,6 +69,28 @@ pub fn welcome<'a>() -> Element<'a, Message> {
                                     )
                                     .padding(20),
                                 )
+                                .push(
+                                    Container::new(
+                                        Column::new()
+                                            .spacing(20)
+                                            .align_items(Alignment::Center)
+                                            .push(
+                                                image::participate_in_new_wallet_icon()
+                                                    .width(Length::Fixed(200.0)),
+                                            )
+                                            .push(
+                                                p1_regular("Participate in new wallet")
+                                                    .style(color::GREY_3),
+                                            )
+                                            .push(
+                                                button::secondary(None, "Select")
+                                                    .width(Length::Fixed(200.0))
+                                                    .on_press(Message::ParticipateWallet),
+                                            )
+                                            .align_items(Alignment::Center),
+                                    )
+                                    .padding(20),
+                                )
                                 .push(
                                     Container::new(
                                         Column::new()
@@ -104,8 +101,7 @@ pub fn welcome<'a>() -> Element<'a, Message> {
                                                     .width(Length::Fixed(100.0)),
                                             )
                                             .push(
-                                                p1_regular("Add an existing wallet")
-                                                    .style(color::GREY_3),
+                                                p1_regular("Restore a wallet").style(color::GREY_3),
                                             )
                                             .push(
                                                 button::secondary(None, "Select")
@@ -117,6 +113,11 @@ pub fn welcome<'a>() -> Element<'a, Message> {
                                     .padding(20),
                                 ),
                         )
+                        .push(
+                            button::secondary(Some(icon::previous_icon()), "Change network")
+                                .width(Length::Fixed(200.0))
+                                .on_press(Message::BackToLauncher),
+                        )
                         .push(Space::with_height(Length::Fixed(100.0)))
                         .spacing(50)
                         .align_items(Alignment::Center),
@@ -422,22 +423,18 @@ pub fn import_descriptor<'a>(
     )
 }
 
-const BACKUP_WARNING: &str =
-    "Beware to back up the mnemonic as it will NOT be stored on the computer.";
-
-pub fn signer_xpubs<'a>(
-    xpubs: &'a [String],
-    words: &'a [&'static str; 12],
-    did_backup: bool,
-) -> Element<'a, Message> {
+pub fn signer_xpubs(xpubs: &[String]) -> Element<Message> {
     Container::new(
         Column::new()
             .push(
                 Button::new(
                     Row::new().align_items(Alignment::Center).push(
                         Column::new()
-                            .push(text("Generate a new mnemonic").bold())
-                            .push(text(BACKUP_WARNING).small().style(color::ORANGE))
+                            .push(text("This computer").bold())
+                            .push(
+                                text("Derive a key from a mnemonic stored on this computer")
+                                    .small(),
+                            )
                             .spacing(5)
                             .width(Length::Fill),
                     ),
@@ -455,39 +452,6 @@ pub fn signer_xpubs<'a>(
             .push_maybe(if xpubs.is_empty() {
                 None
             } else {
-                Some(
-                    Container::new(words.iter().enumerate().fold(
-                        Column::new().spacing(5),
-                        |acc, (i, w)| {
-                            acc.push(
-                                Row::new()
-                                    .align_items(Alignment::End)
-                                    .push(
-                                        Container::new(text(format!("#{}", i + 1)).small())
-                                            .width(Length::Fixed(50.0)),
-                                    )
-                                    .push(text(*w).bold()),
-                            )
-                        },
-                    ))
-                    .padding(15),
-                )
-            })
-            .push_maybe(if !xpubs.is_empty() {
-                Some(
-                    Container::new(
-                        checkbox(
-                            "I have backed up the mnemonic, show the extended public key",
-                            did_backup,
-                        )
-                        .on_toggle(Message::UserActionDone),
-                    )
-                    .padding(10),
-                )
-            } else {
-                None
-            })
-            .push_maybe(if !xpubs.is_empty() && did_backup {
                 Some(xpubs.iter().fold(Column::new().padding(15), |col, xpub| {
                     col.push(
                         Row::new()
@@ -512,8 +476,6 @@ pub fn signer_xpubs<'a>(
                             ),
                     )
                 }))
-            } else {
-                None
             }),
     )
     .style(theme::Container::Card(theme::Card::Simple))
@@ -604,31 +566,42 @@ pub fn hardware_wallet_xpubs<'a>(
     .into()
 }
 
-pub fn share_xpubs<'a>(
+pub fn participate_xpub<'a>(
+    progress: (usize, usize),
     hws: Vec<Element<'a, Message>>,
     signer: Element<'a, Message>,
+    shared: bool,
 ) -> Element<'a, Message> {
     layout(
-        (0, 0),
-        "Share your public keys (Xpubs)",
+        progress,
+        "Share your public keys",
         Column::new()
             .push(
-                Container::new(
-                    text("Import an extended public key by selecting a signing device:").bold(),
-                )
-                .width(Length::Fill),
+                Column::new()
+                    .push(
+                        Container::new(
+                            text("Generate an extended public key by selecting a signing device:")
+                                .bold(),
+                        )
+                        .width(Length::Fill),
+                    )
+                    .spacing(10)
+                    .push(Column::with_children(hws).spacing(10))
+                    .push(signer)
+                    .width(Length::Fill),
+            )
+            .push(
+                checkbox("I have shared my extended public key", shared)
+                    .on_toggle(Message::UserActionDone),
             )
-            .push_maybe(if hws.is_empty() {
-                Some(p1_regular("No signing device connected").style(color::GREY_3))
+            .push(if shared {
+                button::primary(None, "Next")
+                    .width(Length::Fixed(200.0))
+                    .on_press(Message::Next)
             } else {
-                None
+                button::primary(None, "Next").width(Length::Fixed(200.0))
             })
-            .spacing(10)
-            .push(Column::with_children(hws).spacing(10))
-            .push(Container::new(text("Or create a new random key:").bold()).width(Length::Fill))
-            .push(signer)
-            .push(Space::with_height(10))
-            .width(Length::Fill),
+            .spacing(50),
         true,
         Some(Message::Previous),
     )
@@ -777,11 +750,7 @@ pub fn register_descriptor<'a>(
             })
             .spacing(50),
         true,
-        if !processing {
-        Some(Message::Previous)
-        } else {
-            None
-        }
+        Some(Message::Previous),
     )
 }
 
@@ -1978,15 +1947,11 @@ fn layout<'a>(
                             .center_x(),
                     )
                     .push(Container::new(h3(title)).width(Length::FillPortion(8)))
-                    .push_maybe(if progress.1 > 0 {
-                        Some(
-                            Container::new(text(format!("{} | {}", progress.0, progress.1)))
-                                .width(Length::FillPortion(2))
-                                .center_x(),
-                        )
-                    } else {
-                        None
-                    }),
+                    .push(
+                        Container::new(text(format!("{} | {}", progress.0, progress.1)))
+                            .width(Length::FillPortion(2))
+                            .center_x(),
+                    ),
             )
             .push(
                 Row::new()
diff --git a/gui/src/lianalite/client/auth.rs b/gui/src/lianalite/client/auth.rs
deleted file mode 100644
index 0d0412a9..00000000
--- a/gui/src/lianalite/client/auth.rs
+++ /dev/null
@@ -1,176 +0,0 @@
-use reqwest::{Error, IntoUrl, Method, RequestBuilder, Response};
-use serde::{Deserialize, Serialize};
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct SignInOtp<'a> {
-    email: &'a str,
-    create_user: bool,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct VerifyOtp<'a, 'b> {
-    email: &'a str,
-    token: &'b str,
-    #[serde(rename = "type")]
-    kind: &'static str,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct ResendOtp<'a> {
-    email: &'a str,
-    #[serde(rename = "type")]
-    kind: &'static str,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct RefreshToken<'a> {
-    refresh_token: &'a str,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct AccessTokenResponse {
-    pub access_token: String,
-    pub expires_at: i64,
-    pub refresh_token: String,
-}
-
-#[derive(Debug, Clone)]
-pub struct AuthClient {
-    http: reqwest::Client,
-    url: String,
-    api_public_key: String,
-}
-
-#[derive(Debug, Clone)]
-pub struct AuthError {
-    pub http_status: Option<u16>,
-    pub error: String,
-}
-
-impl std::fmt::Display for AuthError {
-    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
-        if let Some(status) = self.http_status {
-            write!(f, "{}: {}", status, self.error)
-        } else {
-            write!(f, "{}", self.error)
-        }
-    }
-}
-
-impl From<Error> for AuthError {
-    fn from(value: Error) -> Self {
-        AuthError {
-            http_status: None,
-            error: value.to_string(),
-        }
-    }
-}
-
-impl AuthClient {
-    pub fn new(url: String, api_public_key: String) -> Self {
-        AuthClient {
-            http: reqwest::Client::new(),
-            url,
-            api_public_key,
-        }
-    }
-
-    fn request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder {
-        let req = self
-            .http
-            .request(method, url)
-            .header("apikey", &self.api_public_key)
-            .header("Content-Type", "application/json");
-        tracing::debug!("Sending http request: {:?}", req);
-        req
-    }
-
-    pub async fn sign_in_otp(&self, email: &str) -> Result<(), AuthError> {
-        let response: Response = self
-            .request(Method::POST, &format!("{}/auth/v1/otp", self.url))
-            .json(&SignInOtp {
-                email,
-                create_user: true,
-            })
-            .send()
-            .await?;
-
-        if !response.status().is_success() {
-            return Err(AuthError {
-                http_status: Some(response.status().into()),
-                error: response.text().await?,
-            });
-        }
-
-        Ok(())
-    }
-
-    pub async fn resend_otp(&self, email: &str) -> Result<Response, AuthError> {
-        let response: Response = self
-            .request(Method::POST, &format!("{}/auth/v1/resend", self.url))
-            .json(&ResendOtp {
-                email,
-                kind: "email",
-            })
-            .send()
-            .await?;
-        if !response.status().is_success() {
-            return Err(AuthError {
-                http_status: Some(response.status().into()),
-                error: response.text().await?,
-            });
-        }
-        Ok(response)
-    }
-
-    pub async fn verify_otp(
-        &self,
-        email: &str,
-        token: &str,
-    ) -> Result<AccessTokenResponse, AuthError> {
-        let response: Response = self
-            .http
-            .post(&format!("{}/auth/v1/verify", self.url))
-            .header("apikey", &self.api_public_key)
-            .header("Content-Type", "application/json")
-            .json(&VerifyOtp {
-                email,
-                token,
-                kind: "email",
-            })
-            .send()
-            .await?;
-        if !response.status().is_success() {
-            return Err(AuthError {
-                http_status: Some(response.status().into()),
-                error: response.text().await?,
-            });
-        }
-
-        Ok(response.json().await?)
-    }
-
-    pub async fn refresh_token(
-        &self,
-        refresh_token: &str,
-    ) -> Result<AccessTokenResponse, AuthError> {
-        let response: Response = self
-            .http
-            .post(&format!(
-                "{}/auth/v1/token?grant_type=refresh_token",
-                self.url
-            ))
-            .header("apikey", &self.api_public_key)
-            .header("Content-Type", "application/json")
-            .json(&RefreshToken { refresh_token })
-            .send()
-            .await?;
-        if !response.status().is_success() {
-            return Err(AuthError {
-                http_status: Some(response.status().into()),
-                error: response.text().await?,
-            });
-        }
-        Ok(response.json().await?)
-    }
-}
diff --git a/gui/src/lianalite/client/backend/api.rs b/gui/src/lianalite/client/backend/api.rs
deleted file mode 100644
index 6649418a..00000000
--- a/gui/src/lianalite/client/backend/api.rs
+++ /dev/null
@@ -1,416 +0,0 @@
-use std::collections::HashMap;
-use std::str::FromStr;
-
-use liana::{
-    descriptors::LianaDescriptor,
-    miniscript::bitcoin::{self, bip32, consensus, hashes::hex::FromHex, Amount, OutPoint, Txid},
-};
-use serde::{de, Deserialize, Deserializer};
-
-pub fn deser_fromstr<'de, D, T>(deserializer: D) -> Result<T, D::Error>
-where
-    D: Deserializer<'de>,
-    T: FromStr,
-    <T as FromStr>::Err: std::fmt::Display,
-{
-    let string = String::deserialize(deserializer)?;
-    T::from_str(&string).map_err(de::Error::custom)
-}
-
-/// Deserialize an address from string, assuming the network was checked.
-pub fn deser_addr_assume_checked<'de, D>(deserializer: D) -> Result<bitcoin::Address, D::Error>
-where
-    D: Deserializer<'de>,
-{
-    let string = String::deserialize(deserializer)?;
-    bitcoin::Address::from_str(&string)
-        .map(|addr| addr.assume_checked())
-        .map_err(de::Error::custom)
-}
-
-/// Deserialize an amount from sats
-pub fn deser_amount_from_sats<'de, D>(deserializer: D) -> Result<bitcoin::Amount, D::Error>
-where
-    D: Deserializer<'de>,
-{
-    let a = u64::deserialize(deserializer)?;
-    Ok(bitcoin::Amount::from_sat(a))
-}
-
-pub fn deser_hex<'de, D, T>(d: D) -> Result<T, D::Error>
-where
-    D: Deserializer<'de>,
-    T: consensus::Decodable,
-{
-    let s = String::deserialize(d)?;
-    let s = Vec::from_hex(&s).map_err(de::Error::custom)?;
-    consensus::deserialize(&s).map_err(de::Error::custom)
-}
-
-/// The maximum number of item to return.
-pub const DEFAULT_LIMIT: usize = 20;
-/// The maximum number of outpoints that can be provided as a filter.
-pub const DEFAULT_OUTPOINTS_LIMIT: usize = 50;
-/// The maximum number of items that can be provided as a filter.
-pub const DEFAULT_LABEL_ITEMS_LIMIT: usize = 50;
-
-#[derive(Deserialize)]
-pub struct Claims {
-    pub sub: String,
-}
-
-#[derive(Deserialize)]
-pub struct NetworkInfo {
-    pub feerate: Feerate,
-    pub rates: HashMap<String, f32>,
-}
-
-#[derive(Deserialize)]
-pub struct Feerate {
-    pub low: Option<i32>,
-    pub high: Option<i32>,
-}
-
-#[derive(Debug, Clone, Deserialize)]
-pub struct WalletBalance {
-    /// Total of funds that present in a block.
-    pub confirmed: u64,
-    /// Total of funds that is not yet in a block.
-    pub unconfirmed: u64,
-    /// Total of funds that are mined but not yet available
-    pub immature: u64,
-    /// Total of funds that are unconfirmed but are coming from
-    /// the wallet
-    pub unconfirmed_change: u64,
-}
-
-#[derive(Debug, Clone, Deserialize)]
-#[serde(rename_all = "lowercase")]
-pub enum WalletStatus {
-    Normal,
-    Recovering,
-    Recovered,
-}
-
-#[derive(Debug, Clone, Deserialize)]
-pub struct RecoveryPath {
-    pub sequence: u16,
-    pub available_balance: u64,
-    pub total_coins: usize,
-}
-
-#[derive(Debug, Clone, Deserialize)]
-pub struct Wallet {
-    pub id: String,
-    pub name: String,
-    #[serde(deserialize_with = "deser_fromstr")]
-    pub descriptor: LianaDescriptor,
-    pub recovery_paths: Vec<RecoveryPath>,
-    pub biggest_remaining_sequence: Option<u32>,
-    pub smallest_remaining_sequence: Option<u32>,
-    pub metadata: WalletMetadata,
-    pub created_at: i64,
-    pub balance: WalletBalance,
-    pub status: WalletStatus,
-    pub tip_height: Option<i32>,
-}
-
-#[derive(Deserialize)]
-pub struct ListWallets {
-    pub wallets: Vec<Wallet>,
-}
-
-#[derive(Debug, Clone, Deserialize)]
-pub struct WalletMetadata {
-    pub ledger_hmacs: Vec<LedgerHmac>,
-    pub fingerprint_aliases: Vec<FingerprintAlias>,
-}
-
-#[derive(Debug, Clone, Deserialize)]
-pub struct LedgerHmac {
-    #[serde(deserialize_with = "deser_fromstr")]
-    pub fingerprint: bip32::Fingerprint,
-    pub user_id: String,
-    pub hmac: String,
-}
-
-#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
-pub struct FingerprintAlias {
-    #[serde(deserialize_with = "deser_fromstr")]
-    pub fingerprint: bip32::Fingerprint,
-    pub user_id: String,
-    pub alias: String,
-}
-
-#[derive(Deserialize)]
-pub struct WalletLabels {
-    pub labels: HashMap<String, String>,
-}
-
-#[derive(serde::Deserialize)]
-#[serde(rename_all = "lowercase")]
-pub enum PaymentKind {
-    Outgoing,
-    Incoming,
-}
-
-#[derive(Deserialize)]
-pub struct Payment {
-    pub txuuid: String,
-    pub txid: String,
-    pub vout: u32,
-    pub amount: u64,
-    pub block_height: Option<i32>,
-    pub confirmed_at: Option<i64>,
-    pub label: Option<String>,
-    pub address_label: Option<String>,
-    pub transaction_label: Option<String>,
-    pub kind: PaymentKind,
-    pub is_single: bool,
-}
-
-#[derive(Deserialize)]
-pub struct ListPayments {
-    pub payments: Vec<Payment>,
-}
-
-#[derive(Clone, Deserialize)]
-pub struct Coin {
-    #[serde(deserialize_with = "deser_addr_assume_checked")]
-    pub address: bitcoin::Address,
-    #[serde(deserialize_with = "deser_amount_from_sats")]
-    pub amount: Amount,
-    pub derivation_index: bip32::ChildNumber,
-    pub outpoint: OutPoint,
-    pub block_height: Option<i32>,
-    pub spend_info: Option<CoinSpendInfo>,
-    pub is_immature: bool,
-    pub is_change_address: bool,
-}
-
-#[derive(Clone, Deserialize)]
-pub struct CoinSpendInfo {
-    pub txid: Txid,
-    pub height: Option<i32>,
-}
-
-#[derive(Deserialize)]
-pub struct ListCoins {
-    pub coins: Vec<Coin>,
-}
-
-#[derive(Clone, Deserialize, PartialEq, Eq)]
-#[serde(rename_all = "lowercase")]
-pub enum UTXOKind {
-    Deposit,
-    Change,
-    External,
-}
-
-#[derive(Clone, Deserialize)]
-pub struct Transaction {
-    pub uuid: String,
-    pub txid: String,
-    pub fee: u64,
-    pub fee_rate: u64,
-    pub block_height: Option<i32>,
-    pub confirmed_at: Option<i64>,
-    pub label: Option<String>,
-    #[serde(deserialize_with = "deser_hex")]
-    pub raw: bitcoin::Transaction,
-    pub inputs: Vec<Input>,
-    pub outputs: Vec<Output>,
-    /// If the transaction has multiple incoming or ougoing payment.
-    pub is_batch: bool,
-}
-
-#[derive(Deserialize)]
-pub struct ListTransactions {
-    pub transactions: Vec<Transaction>,
-}
-
-#[derive(Clone, Deserialize)]
-pub struct Output {
-    pub address: Option<String>,
-    pub label: Option<String>,
-    pub address_label: Option<String>,
-    pub amount: u64,
-    pub kind: UTXOKind,
-    pub coin: Option<Coin>,
-}
-
-#[derive(Clone, Deserialize)]
-pub struct Input {
-    pub txid: String,
-    pub vout: usize,
-    pub amount: Option<u64>,
-    pub label: Option<String>,
-    pub kind: UTXOKind,
-    pub coin: Option<Coin>,
-}
-
-#[derive(Clone, Deserialize)]
-pub struct Psbt {
-    pub uuid: String,
-    pub txid: Txid,
-    pub fee: Option<u64>,
-    pub fee_rate: Option<u64>,
-    pub label: Option<String>,
-    #[serde(deserialize_with = "deser_fromstr")]
-    pub raw: bitcoin::Psbt,
-    pub inputs: Vec<Input>,
-    pub outputs: Vec<Output>,
-    pub is_batch: bool,
-    pub updated_at: i64,
-}
-
-#[derive(Clone, Deserialize)]
-#[serde(untagged)]
-pub enum DraftPsbtResult {
-    Success(DraftPsbt),
-    InsufficientFunds(InsufficientFundsInfo),
-}
-
-#[derive(Clone, Deserialize)]
-pub struct InsufficientFundsInfo {
-    pub missing: u64,
-}
-
-#[derive(Clone, Deserialize)]
-pub struct DraftPsbt {
-    pub uuid: Option<String>,
-    pub txid: Txid,
-    pub fee: u64,
-    pub fee_rate: u64,
-    pub label: Option<String>,
-    #[serde(deserialize_with = "deser_fromstr")]
-    pub raw: bitcoin::Psbt,
-    pub inputs: Vec<Input>,
-    pub outputs: Vec<Output>,
-    pub warnings: Vec<String>,
-}
-
-#[derive(Deserialize)]
-pub struct ListPsbts {
-    pub psbts: Vec<Psbt>,
-}
-
-#[derive(Deserialize)]
-pub struct Address {
-    #[serde(deserialize_with = "deser_addr_assume_checked")]
-    pub address: bitcoin::Address,
-    pub derivation_index: bip32::ChildNumber,
-}
-
-pub mod payload {
-    use liana::miniscript::bitcoin;
-    use serde::{Serialize, Serializer};
-
-    pub fn ser_to_string<T: std::fmt::Display, S: Serializer>(
-        field: T,
-        s: S,
-    ) -> Result<S::Ok, S::Error> {
-        s.serialize_str(&field.to_string())
-    }
-
-    #[derive(Serialize)]
-    pub struct ImportPsbt {
-        pub psbt: String,
-    }
-
-    #[derive(Serialize)]
-    pub struct Recipient {
-        /// Recipient cannot have an empty amount and is_max set to false
-        /// Amount cannot be less that the DUST limit.
-        pub amount: Option<u64>,
-        pub address: bitcoin::Address<bitcoin::address::NetworkUnchecked>,
-        /// If is_max is set to true, API will calculate the remaining funds and
-        /// use it for psbt output amount.
-        /// Only one recipient can have is_max set to true
-        pub is_max: bool,
-    }
-
-    #[derive(Serialize)]
-    pub struct GeneratePsbt<'a> {
-        pub recipients: Vec<Recipient>,
-        /// The outpoints of coins to use as transaction inputs. If empty,
-        /// coins will be selected automatically from the set of confirmed coins
-        /// and those unconfirmed coins at a change address, excluding immature
-        /// coins.
-        pub inputs: &'a [bitcoin::OutPoint],
-        // The feerate to use for this transaction.
-        pub feerate: u64,
-        /// If save is set to true, API will save in database the generated psbt
-        /// and store the generated change address.
-        pub save: bool,
-    }
-
-    #[derive(Serialize)]
-    pub struct GenerateRecoveryPsbt {
-        /// The address to sweep funds to.
-        pub address: bitcoin::Address<bitcoin::address::NetworkUnchecked>,
-        // The feerate to use for this transaction.
-        pub feerate: u64,
-        /// Timelock of the recovery path to use.
-        pub timelock: u16,
-        /// If save is set to true, API will save in database the generated psbt
-        /// and store the generated change address.
-        pub save: bool,
-    }
-
-    #[derive(Serialize)]
-    pub struct Labels {
-        pub labels: Vec<Label>,
-    }
-
-    #[derive(Serialize)]
-    pub struct Label {
-        pub item: String,
-        pub value: Option<String>,
-    }
-
-    #[derive(Serialize)]
-    pub struct GenerateRbfPsbt {
-        /// ID of the transaction to be replaced.
-        #[serde(serialize_with = "ser_to_string")]
-        pub txid: bitcoin::Txid,
-        /// The target feerate (sat/vb) to use for the replacement transaction
-        /// in order to bump the fee of the transaction being replaced.
-        ///
-        /// Must be provided if and only if `is_cancel` is `false`.
-        pub feerate: Option<u64>,
-        /// Whether to cancel the transaction.
-        ///
-        /// If `true`, the feerate of the replacement transaction will be set
-        /// automatically to the lowest possible feerate that satisfies all
-        /// RBF policies.
-        ///
-        /// If `false`, the transaction will be replaced by another at the target
-        /// `feerate` in order to bump its fee.
-        pub is_cancel: bool,
-        /// If save is set to true, API will save in database the generated psbt
-        /// and, if a new change address is generated for the replacement, store
-        /// this also. Note that if the transaction being replaced has a change
-        /// output, then its corresponding change address will be reused in the
-        /// replacement.
-        pub save: bool,
-    }
-
-    #[derive(Serialize)]
-    pub struct UpdateWallet {
-        pub ledger_hmac: Option<UpdateLedgerHmac>,
-        pub fingerprint_aliases: Option<Vec<UpdateFingerprintAlias>>,
-    }
-
-    #[derive(Serialize)]
-    pub struct UpdateLedgerHmac {
-        pub fingerprint: String,
-        pub hmac: String,
-    }
-
-    #[derive(Serialize)]
-    pub struct UpdateFingerprintAlias {
-        pub fingerprint: String,
-        pub alias: String,
-    }
-}
diff --git a/gui/src/lianalite/client/backend/mod.rs b/gui/src/lianalite/client/backend/mod.rs
deleted file mode 100644
index 6bff0861..00000000
--- a/gui/src/lianalite/client/backend/mod.rs
+++ /dev/null
@@ -1,1043 +0,0 @@
-pub mod api;
-
-use std::{
-    collections::{HashMap, HashSet},
-    sync::Arc,
-};
-
-use async_trait::async_trait;
-use chrono::Utc;
-use liana::{
-    commands::{CoinStatus, GetInfoDescriptors, LCSpendInfo, LabelItem},
-    config::Config,
-    descriptors::LianaDescriptor,
-    miniscript::bitcoin::{address, psbt::Psbt, Address, Network, OutPoint, Txid},
-};
-use reqwest::{Error, IntoUrl, Method, RequestBuilder, Response};
-use tokio::sync::RwLock;
-
-use crate::{
-    daemon::{model::*, Daemon, DaemonBackend, DaemonError},
-    hw::HardwareWalletConfig,
-};
-
-use self::api::{UTXOKind, DEFAULT_OUTPOINTS_LIMIT};
-
-use super::auth::{self, AccessTokenResponse, AuthError};
-
-impl From<Error> for DaemonError {
-    fn from(value: Error) -> Self {
-        DaemonError::Http(None, value.to_string())
-    }
-}
-
-impl From<AuthError> for DaemonError {
-    fn from(value: AuthError) -> Self {
-        DaemonError::Http(value.http_status, value.error)
-    }
-}
-
-fn request<U: IntoUrl>(
-    http: &reqwest::Client,
-    method: Method,
-    url: U,
-    access_token: &str,
-) -> RequestBuilder {
-    let req = http
-        .request(method, url)
-        .header("Authorization", format!("Bearer {}", access_token))
-        .header("Content-Type", "application/json");
-    tracing::debug!("Sending http request: {:?}", req);
-    req
-}
-
-#[derive(Debug, Clone)]
-pub struct BackendClient {
-    auth: Arc<RwLock<auth::AccessTokenResponse>>,
-    auth_client: auth::AuthClient,
-
-    url: String,
-    network: Network,
-    http: reqwest::Client,
-
-    user_id: String,
-}
-
-impl BackendClient {
-    pub async fn connect(
-        auth_client: auth::AuthClient,
-        url: String,
-        credentials: auth::AccessTokenResponse,
-    ) -> Result<Self, DaemonError> {
-        let http = reqwest::Client::new();
-        let response = request(
-            &http,
-            Method::GET,
-            &format!("{}/v1/me", url),
-            &credentials.access_token,
-        )
-        .send()
-        .await?;
-        if !response.status().is_success() {
-            return Err(DaemonError::NoAnswer);
-        }
-        let res: api::Claims = response.json().await?;
-        let user_id = res.sub;
-
-        Ok(Self {
-            auth: Arc::new(RwLock::new(credentials)),
-            auth_client,
-            network: Network::Signet,
-            url,
-            user_id,
-            http,
-        })
-    }
-
-    pub async fn connect_first(self) -> Result<(BackendWalletClient, api::Wallet), DaemonError> {
-        let wallets = self.list_wallets().await?;
-        let first = wallets.first().cloned().ok_or(DaemonError::NoAnswer)?;
-        Ok((
-            BackendWalletClient {
-                inner: self,
-                curve: secp256k1::Secp256k1::verification_only(),
-                wallet_uuid: first.id.clone(),
-                wallet_desc: first.descriptor.to_owned(),
-            },
-            first,
-        ))
-    }
-
-    async fn request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder {
-        let access_token = &self.auth.read().await.access_token;
-        request(&self.http, method, url, access_token)
-    }
-
-    pub async fn list_wallets(&self) -> Result<Vec<api::Wallet>, DaemonError> {
-        let response = self
-            .request(Method::GET, &format!("{}/v1/wallets", self.url))
-            .await
-            .send()
-            .await?;
-        if !response.status().is_success() {
-            return Err(DaemonError::Http(
-                Some(response.status().into()),
-                response.text().await?,
-            ));
-        }
-
-        let list: api::ListWallets = response.json().await?;
-        Ok(list.wallets)
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct BackendWalletClient {
-    inner: BackendClient,
-    wallet_uuid: String,
-    wallet_desc: LianaDescriptor,
-    curve: secp256k1::Secp256k1<secp256k1::VerifyOnly>,
-}
-
-impl BackendWalletClient {
-    pub fn user_id(&self) -> &str {
-        &self.inner.user_id
-    }
-
-    async fn get_wallet(&self) -> Result<api::Wallet, DaemonError> {
-        let list = self.inner.list_wallets().await?;
-        let wallet = list
-            .into_iter()
-            .find(|w| w.id == self.wallet_uuid)
-            .ok_or(DaemonError::Unexpected("No wallet".to_string()))?;
-        Ok(wallet)
-    }
-
-    async fn list_psbts(&self, txids: &[Txid]) -> Result<api::ListPsbts, DaemonError> {
-        let mut query = Vec::<(&str, String)>::new();
-        if !txids.is_empty() {
-            query.push((
-                "txids",
-                txids
-                    .iter()
-                    .map(|i| i.to_string())
-                    .collect::<Vec<String>>()
-                    .join(","),
-            ))
-        }
-        let response: Response = self
-            .inner
-            .request(
-                Method::GET,
-                &format!("{}/v1/wallets/{}/psbts", self.inner.url, self.wallet_uuid),
-            )
-            .await
-            .query(&query)
-            .send()
-            .await?;
-
-        if !response.status().is_success() {
-            return Err(DaemonError::Http(
-                Some(response.status().into()),
-                response.text().await?,
-            ));
-        }
-
-        response.json().await.map_err(DaemonError::from)
-    }
-
-    async fn list_txs_by_txids(
-        &self,
-        txids: &[Txid],
-    ) -> Result<api::ListTransactions, DaemonError> {
-        let mut query = Vec::<(&str, String)>::new();
-        if !txids.is_empty() {
-            query.push((
-                "txids",
-                txids
-                    .iter()
-                    .map(|i| i.to_string())
-                    .collect::<Vec<String>>()
-                    .join(","),
-            ))
-        } else {
-            return Ok(api::ListTransactions {
-                transactions: Vec::new(),
-            });
-        }
-        let response: Response = self
-            .inner
-            .request(
-                Method::GET,
-                &format!(
-                    "{}/v1/wallets/{}/transactions",
-                    self.inner.url, self.wallet_uuid
-                ),
-            )
-            .await
-            .query(&query)
-            .send()
-            .await?;
-
-        if !response.status().is_success() {
-            return Err(DaemonError::Http(
-                Some(response.status().into()),
-                response.text().await?,
-            ));
-        }
-
-        response.json().await.map_err(DaemonError::from)
-    }
-
-    async fn list_wallet_txs(
-        &self,
-        before: Option<u32>,
-        limit: Option<u64>,
-    ) -> Result<api::ListTransactions, DaemonError> {
-        let mut query = Vec::<(&str, String)>::new();
-        if let Some(before) = before {
-            query.push(("before", before.to_string()))
-        }
-        if let Some(limit) = limit {
-            query.push(("limit", limit.to_string()))
-        }
-        let response: Response = self
-            .inner
-            .request(
-                Method::GET,
-                &format!(
-                    "{}/v1/wallets/{}/transactions",
-                    self.inner.url, self.wallet_uuid
-                ),
-            )
-            .await
-            .query(&query)
-            .send()
-            .await?;
-
-        if !response.status().is_success() {
-            return Err(DaemonError::Http(
-                Some(response.status().into()),
-                response.text().await?,
-            ));
-        }
-
-        response.json().await.map_err(DaemonError::from)
-    }
-
-    async fn list_wallet_coins(
-        &self,
-        statuses: &[CoinStatus],
-        outpoints: &[OutPoint],
-    ) -> Result<api::ListCoins, DaemonError> {
-        let mut query = Vec::<(&'static str, String)>::new();
-        if !statuses.is_empty() {
-            query.push((
-                "statuses",
-                statuses
-                    .iter()
-                    .map(|s| s.to_arg())
-                    .collect::<Vec<&str>>()
-                    .join(","),
-            ));
-        }
-        if !outpoints.is_empty() {
-            query.push((
-                "outpoints",
-                outpoints
-                    .iter()
-                    .map(|o| o.to_string())
-                    .collect::<Vec<String>>()
-                    .join(","),
-            ));
-        }
-        let response: Response = self
-            .inner
-            .request(
-                Method::GET,
-                &format!("{}/v1/wallets/{}/coins", self.inner.url, self.wallet_uuid),
-            )
-            .await
-            .query(&query)
-            .send()
-            .await?;
-
-        if !response.status().is_success() {
-            return Err(DaemonError::Http(
-                Some(response.status().into()),
-                response.text().await?,
-            ));
-        }
-
-        let res: api::ListCoins = response.json().await?;
-        Ok(res)
-    }
-
-    async fn auth(&self) -> AccessTokenResponse {
-        self.inner.auth.read().await.clone()
-    }
-}
-
-#[async_trait]
-impl Daemon for BackendWalletClient {
-    fn backend(&self) -> DaemonBackend {
-        DaemonBackend::RemoteBackend
-    }
-
-    fn config(&self) -> Option<&Config> {
-        None
-    }
-
-    /// refresh the token if close to expiration.
-    async fn is_alive(&self) -> Result<(), DaemonError> {
-        let auth = self.auth().await;
-        if auth.expires_at < Utc::now().timestamp() + 60 {
-            match self.inner.auth.try_write() {
-                Err(_) => {
-                    // something is using the lock, we will try next time.
-                    return Ok(());
-                }
-                Ok(mut old) => {
-                    let new = self
-                        .inner
-                        .auth_client
-                        .refresh_token(&auth.refresh_token)
-                        .await?;
-
-                    *old = new;
-                    tracing::info!("Liana backend access was refreshed");
-                }
-            }
-        }
-        Ok(())
-    }
-
-    async fn stop(&self) -> Result<(), DaemonError> {
-        Ok(())
-    }
-
-    async fn get_info(&self) -> Result<GetInfoResult, DaemonError> {
-        let wallet = self.get_wallet().await?;
-
-        Ok(GetInfoResult {
-            network: self.inner.network,
-            version: "".to_string(),
-            block_height: wallet.tip_height.unwrap_or(0),
-            descriptors: GetInfoDescriptors {
-                main: wallet.descriptor.to_owned(),
-            },
-            sync: 1.0,
-            rescan_progress: None,
-            timestamp: wallet.created_at as u32,
-        })
-    }
-
-    async fn get_new_address(&self) -> Result<GetAddressResult, DaemonError> {
-        let response: Response = self
-            .inner
-            .request(
-                Method::POST,
-                &format!(
-                    "{}/v1/wallets/{}/addresses",
-                    self.inner.url, self.wallet_uuid
-                ),
-            )
-            .await
-            .send()
-            .await?;
-
-        if !response.status().is_success() {
-            return Err(DaemonError::Http(
-                Some(response.status().into()),
-                response.text().await?,
-            ));
-        }
-
-        let res: api::Address = response.json().await?;
-        Ok(GetAddressResult {
-            address: res.address,
-            derivation_index: res.derivation_index,
-        })
-    }
-
-    /// Spent coins are not returned if statuses is empty, unless their outpoints are specified.
-    async fn list_coins(
-        &self,
-        statuses: &[CoinStatus],
-        outpoints: &[OutPoint],
-    ) -> Result<ListCoinsResult, DaemonError> {
-        let coins = if !outpoints.is_empty() {
-            let mut coins = Vec::new();
-            for chunk in outpoints.chunks(DEFAULT_OUTPOINTS_LIMIT) {
-                coins.extend_from_slice(&self.list_wallet_coins(statuses, chunk).await?.coins);
-            }
-            coins
-        } else {
-            self.list_wallet_coins(statuses, outpoints).await?.coins
-        };
-        Ok(ListCoinsResult {
-            coins: coins
-                .into_iter()
-                .map(|c| ListCoinsEntry {
-                    address: c.address,
-                    amount: c.amount,
-                    derivation_index: c.derivation_index,
-                    outpoint: c.outpoint,
-                    block_height: c.block_height,
-                    is_immature: c.is_immature,
-                    is_change: c.is_change_address,
-                    spend_info: c.spend_info.map(|info| LCSpendInfo {
-                        txid: info.txid,
-                        height: info.height,
-                    }),
-                })
-                .collect(),
-        })
-    }
-
-    async fn list_spend_txs(&self) -> Result<ListSpendResult, DaemonError> {
-        let res = self.list_psbts(&[]).await?;
-        Ok(ListSpendResult {
-            spend_txs: res
-                .psbts
-                .into_iter()
-                .map(|psbt| ListSpendEntry {
-                    psbt: psbt.raw,
-                    updated_at: Some(psbt.updated_at as u32),
-                })
-                .collect(),
-        })
-    }
-
-    async fn list_confirmed_txs(
-        &self,
-        _start: u32,
-        end: u32,
-        limit: u64,
-    ) -> Result<ListTransactionsResult, DaemonError> {
-        let res = self.list_wallet_txs(Some(end), Some(limit)).await?;
-        Ok(ListTransactionsResult {
-            transactions: res
-                .transactions
-                .into_iter()
-                .map(|tx| TransactionInfo {
-                    tx: tx.raw,
-                    height: tx.block_height,
-                    time: tx.confirmed_at.map(|t| t as u32),
-                })
-                .collect(),
-        })
-    }
-
-    async fn list_txs(&self, txids: &[Txid]) -> Result<ListTransactionsResult, DaemonError> {
-        let mut transactions = Vec::new();
-        if !txids.is_empty() {
-            for chunk in txids.chunks(api::DEFAULT_LIMIT) {
-                transactions.extend_from_slice(&self.list_txs_by_txids(chunk).await?.transactions);
-            }
-        }
-        Ok(ListTransactionsResult {
-            transactions: transactions
-                .into_iter()
-                .map(|tx| TransactionInfo {
-                    tx: tx.raw,
-                    height: tx.block_height,
-                    time: tx.confirmed_at.map(|t| t as u32),
-                })
-                .collect(),
-        })
-    }
-
-    async fn create_spend_tx(
-        &self,
-        coins_outpoints: &[OutPoint],
-        destinations: &HashMap<Address<address::NetworkUnchecked>, u64>,
-        feerate_vb: u64,
-        change_address: Option<Address<address::NetworkUnchecked>>,
-    ) -> Result<CreateSpendResult, DaemonError> {
-        let mut recipients: Vec<api::payload::Recipient> = destinations
-            .iter()
-            .map(|(addr, amt)| api::payload::Recipient {
-                amount: Some(*amt),
-                address: addr.clone(),
-                is_max: false,
-            })
-            .collect();
-        if let Some(address) = change_address {
-            recipients.push(api::payload::Recipient {
-                amount: None,
-                address,
-                is_max: true,
-            });
-        }
-        let response: Response = self
-            .inner
-            .request(
-                Method::POST,
-                &format!(
-                    "{}/v1/wallets/{}/psbts/generate",
-                    self.inner.url, self.wallet_uuid
-                ),
-            )
-            .await
-            .json(&api::payload::GeneratePsbt {
-                save: false,
-                feerate: feerate_vb,
-                inputs: coins_outpoints,
-                recipients,
-            })
-            .send()
-            .await?;
-
-        let res: api::DraftPsbtResult = response.json().await?;
-        match res {
-            api::DraftPsbtResult::Success(draft) => Ok(CreateSpendResult::Success {
-                psbt: draft.raw,
-                warnings: draft.warnings,
-            }),
-            api::DraftPsbtResult::InsufficientFunds(api::InsufficientFundsInfo { missing }) => {
-                Ok(CreateSpendResult::InsufficientFunds { missing })
-            }
-        }
-    }
-
-    async fn rbf_psbt(
-        &self,
-        txid: &Txid,
-        is_cancel: bool,
-        feerate_vb: Option<u64>,
-    ) -> Result<CreateSpendResult, DaemonError> {
-        let response: Response = self
-            .inner
-            .request(
-                Method::POST,
-                &format!(
-                    "{}/v1/wallets/{}/psbts/rbf",
-                    self.inner.url, self.wallet_uuid
-                ),
-            )
-            .await
-            .json(&api::payload::GenerateRbfPsbt {
-                txid: *txid,
-                is_cancel,
-                feerate: feerate_vb,
-                save: false,
-            })
-            .send()
-            .await?;
-
-        let res: api::DraftPsbtResult = response.json().await?;
-        match res {
-            api::DraftPsbtResult::Success(draft) => Ok(CreateSpendResult::Success {
-                psbt: draft.raw,
-                warnings: draft.warnings,
-            }),
-            api::DraftPsbtResult::InsufficientFunds(api::InsufficientFundsInfo { missing }) => {
-                Ok(CreateSpendResult::InsufficientFunds { missing })
-            }
-        }
-    }
-
-    async fn update_spend_tx(&self, psbt: &Psbt) -> Result<(), DaemonError> {
-        let response: Response = self
-            .inner
-            .request(
-                Method::POST,
-                &format!("{}/v1/wallets/{}/psbts", self.inner.url, self.wallet_uuid),
-            )
-            .await
-            .json(&api::payload::ImportPsbt {
-                psbt: psbt.to_string(),
-            })
-            .send()
-            .await?;
-
-        if !response.status().is_success() {
-            return Err(DaemonError::Http(
-                Some(response.status().into()),
-                response.text().await?,
-            ));
-        }
-
-        Ok(())
-    }
-
-    async fn delete_spend_tx(&self, txid: &Txid) -> Result<(), DaemonError> {
-        let psbt = self
-            .list_psbts(&[*txid])
-            .await?
-            .psbts
-            .into_iter()
-            .find(|tx| tx.txid == *txid)
-            .ok_or(DaemonError::Http(
-                Some(404),
-                format!("psbt not found with txid: {}", txid),
-            ))?;
-
-        let response: Response = self
-            .inner
-            .request(
-                Method::DELETE,
-                &format!("{}/v1/psbts/{}", self.inner.url, psbt.uuid),
-            )
-            .await
-            .send()
-            .await?;
-
-        if !response.status().is_success() {
-            return Err(DaemonError::Http(
-                Some(response.status().into()),
-                response.text().await?,
-            ));
-        }
-
-        Ok(())
-    }
-
-    async fn broadcast_spend_tx(&self, txid: &Txid) -> Result<(), DaemonError> {
-        let psbt = self
-            .list_psbts(&[*txid])
-            .await?
-            .psbts
-            .into_iter()
-            .find(|tx| tx.txid == *txid)
-            .ok_or(DaemonError::NoAnswer)?;
-
-        let response: Response = self
-            .inner
-            .request(
-                Method::POST,
-                &format!("{}/v1/psbts/{}/broadcast", self.inner.url, psbt.uuid),
-            )
-            .await
-            .send()
-            .await?;
-
-        if !response.status().is_success() {
-            return Err(DaemonError::Http(
-                Some(response.status().into()),
-                response.text().await?,
-            ));
-        }
-
-        Ok(())
-    }
-
-    async fn start_rescan(&self, _t: u32) -> Result<(), DaemonError> {
-        Err(DaemonError::NoAnswer)
-    }
-
-    async fn create_recovery(
-        &self,
-        address: Address<address::NetworkUnchecked>,
-        feerate_vb: u64,
-        sequence: Option<u16>,
-    ) -> Result<Psbt, DaemonError> {
-        let response: Response = self
-            .inner
-            .request(
-                Method::POST,
-                &format!(
-                    "{}/v1/wallets/{}/psbts/recovery",
-                    self.inner.url, self.wallet_uuid
-                ),
-            )
-            .await
-            .json(&api::payload::GenerateRecoveryPsbt {
-                save: false,
-                feerate: feerate_vb,
-                timelock: sequence
-                    .ok_or(DaemonError::Unexpected("Missing sequence".to_string()))?,
-                address,
-            })
-            .send()
-            .await?;
-
-        let res: api::DraftPsbt = response.json().await?;
-        Ok(res.raw)
-    }
-
-    async fn get_labels(
-        &self,
-        items: &HashSet<LabelItem>,
-    ) -> Result<HashMap<String, String>, DaemonError> {
-        if items.is_empty() {
-            return Ok(HashMap::new());
-        }
-        let items: Vec<String> = items.iter().map(|item| item.to_string()).collect();
-        let mut res = HashMap::new();
-        for chunk in items.chunks(api::DEFAULT_LABEL_ITEMS_LIMIT) {
-            let response: Response = self
-                .inner
-                .request(
-                    Method::GET,
-                    &format!("{}/v1/wallets/{}/labels", self.inner.url, self.wallet_uuid),
-                )
-                .await
-                .query(&[("items", chunk.join(","))])
-                .send()
-                .await?;
-
-            if !response.status().is_success() {
-                return Err(DaemonError::Http(
-                    Some(response.status().into()),
-                    response.text().await?,
-                ));
-            }
-
-            let wallet_labels: api::WalletLabels = response.json().await?;
-            res.extend(wallet_labels.labels);
-        }
-
-        Ok(res)
-    }
-
-    async fn update_labels(
-        &self,
-        items: &HashMap<LabelItem, Option<String>>,
-    ) -> Result<(), DaemonError> {
-        let response: Response = self
-            .inner
-            .request(
-                Method::POST,
-                &format!("{}/v1/wallets/{}/labels", self.inner.url, self.wallet_uuid),
-            )
-            .await
-            .json(&api::payload::Labels {
-                labels: items
-                    .iter()
-                    .map(|(item, value)| api::payload::Label {
-                        item: item.to_string(),
-                        value: value.clone(),
-                    })
-                    .collect(),
-            })
-            .send()
-            .await?;
-
-        if !response.status().is_success() {
-            return Err(DaemonError::Http(
-                Some(response.status().into()),
-                response.text().await?,
-            ));
-        }
-
-        Ok(())
-    }
-
-    async fn list_history_txs(
-        &self,
-        _start: u32,
-        end: u32,
-        limit: u64,
-    ) -> Result<Vec<HistoryTransaction>, DaemonError> {
-        let res = self
-            .list_wallet_txs(Some(end), Some(limit))
-            .await?
-            .transactions
-            .into_iter()
-            .map(|tx| history_tx_from_api(tx, self.inner.network))
-            .collect();
-        Ok(res)
-    }
-
-    async fn get_history_txs(
-        &self,
-        txids: &[Txid],
-    ) -> Result<Vec<HistoryTransaction>, DaemonError> {
-        let mut transactions = Vec::new();
-        if !txids.is_empty() {
-            for chunk in txids.chunks(api::DEFAULT_LIMIT) {
-                transactions.extend_from_slice(&self.list_txs_by_txids(chunk).await?.transactions);
-            }
-        }
-        let res = transactions
-            .into_iter()
-            .map(|tx| history_tx_from_api(tx, self.inner.network))
-            .collect();
-        Ok(res)
-    }
-    async fn list_pending_txs(&self) -> Result<Vec<HistoryTransaction>, DaemonError> {
-        let res = self
-            .list_wallet_txs(None, None)
-            .await?
-            .transactions
-            .into_iter()
-            .filter_map(|tx| {
-                if tx.block_height.is_none() {
-                    Some(history_tx_from_api(tx, self.inner.network))
-                } else {
-                    None
-                }
-            })
-            .collect();
-        Ok(res)
-    }
-
-    async fn list_spend_transactions(
-        &self,
-        txids: Option<&[Txid]>,
-    ) -> Result<Vec<SpendTx>, DaemonError> {
-        let mut spend_txs: Vec<SpendTx> = if let Some(txids) = txids {
-            let mut spend_txs = Vec::new();
-            if !txids.is_empty() {
-                for chunk in txids.chunks(api::DEFAULT_LIMIT) {
-                    for tx in self.list_psbts(chunk).await?.psbts.into_iter().map(|tx| {
-                        spend_tx_from_api(tx, &self.wallet_desc, &self.curve, self.inner.network)
-                    }) {
-                        spend_txs.push(tx);
-                    }
-                }
-            }
-            spend_txs
-        } else {
-            self.list_psbts(&[])
-                .await?
-                .psbts
-                .into_iter()
-                .map(|tx| spend_tx_from_api(tx, &self.wallet_desc, &self.curve, self.inner.network))
-                .collect()
-        };
-        spend_txs.sort_by(|a, b| {
-            if a.status == b.status {
-                // last updated first
-                b.updated_at.cmp(&a.updated_at)
-            } else {
-                // follows status enum order
-                a.status.cmp(&b.status)
-            }
-        });
-        Ok(spend_txs)
-    }
-
-    /// Implemented by LianaLite backend
-    async fn update_wallet_metadata(
-        &self,
-        fingerprint_aliases: &HashMap<Fingerprint, String>,
-        hws: &[HardwareWalletConfig],
-    ) -> Result<(), DaemonError> {
-        let wallet = self.get_wallet().await?;
-        let ledger_kinds = [
-            async_hwi::DeviceKind::Ledger.to_string(),
-            async_hwi::DeviceKind::LedgerSimulator.to_string(),
-        ];
-        for cfg in hws {
-            if ledger_kinds.contains(&cfg.kind)
-                && !wallet.metadata.ledger_hmacs.iter().any(|ledger_hmac| {
-                    ledger_hmac.fingerprint == cfg.fingerprint && ledger_hmac.hmac == cfg.token
-                })
-            {
-                let response: Response = self
-                    .inner
-                    .request(
-                        Method::PATCH,
-                        &format!("{}/v1/wallets/{}", self.inner.url, self.wallet_uuid),
-                    )
-                    .await
-                    .json(&api::payload::UpdateWallet {
-                        ledger_hmac: Some(api::payload::UpdateLedgerHmac {
-                            fingerprint: cfg.fingerprint.to_string(),
-                            hmac: cfg.token.clone(),
-                        }),
-                        fingerprint_aliases: None,
-                    })
-                    .send()
-                    .await?;
-
-                if !response.status().is_success() {
-                    return Err(DaemonError::Http(
-                        Some(response.status().into()),
-                        response.text().await?,
-                    ));
-                }
-            }
-        }
-
-        if fingerprint_aliases.iter().any(|(fg, alias)| {
-            !wallet
-                .metadata
-                .fingerprint_aliases
-                .contains(&api::FingerprintAlias {
-                    alias: alias.to_string(),
-                    user_id: self.inner.user_id.clone(),
-                    fingerprint: *fg,
-                })
-        }) {
-            let response: Response = self
-                .inner
-                .request(
-                    Method::PATCH,
-                    &format!("{}/v1/wallets/{}", self.inner.url, self.wallet_uuid),
-                )
-                .await
-                .json(&api::payload::UpdateWallet {
-                    ledger_hmac: None,
-                    fingerprint_aliases: Some(
-                        fingerprint_aliases
-                            .iter()
-                            .map(|(fg, alias)| api::payload::UpdateFingerprintAlias {
-                                fingerprint: fg.to_string(),
-                                alias: alias.to_string(),
-                            })
-                            .collect(),
-                    ),
-                })
-                .send()
-                .await?;
-
-            if !response.status().is_success() {
-                return Err(DaemonError::Http(
-                    Some(response.status().into()),
-                    response.text().await?,
-                ));
-            }
-        }
-
-        Ok(())
-    }
-}
-
-fn history_tx_from_api(value: api::Transaction, network: Network) -> HistoryTransaction {
-    let mut labels = HashMap::<String, Option<String>>::new();
-    let mut coins = Vec::new();
-    for input in &value.inputs {
-        labels.insert(
-            format!("{}:{}", input.txid, input.vout),
-            input.label.clone(),
-        );
-        if input.kind == UTXOKind::Deposit || input.kind == UTXOKind::Change {
-            if let Some(c) = &input.coin {
-                coins.push(ListCoinsEntry {
-                    address: c.address.clone(),
-                    amount: c.amount,
-                    derivation_index: c.derivation_index,
-                    outpoint: c.outpoint,
-                    block_height: c.block_height,
-                    is_immature: c.is_immature,
-                    is_change: c.is_change_address,
-                    spend_info: c.spend_info.clone().map(|info| LCSpendInfo {
-                        txid: info.txid,
-                        height: info.height,
-                    }),
-                });
-            }
-        }
-    }
-    let mut changes_indexes = Vec::new();
-    let txid = value.raw.txid().to_string();
-    for (index, output) in value.outputs.iter().enumerate() {
-        labels.insert(format!("{}:{}", txid, index), output.label.clone());
-        if let Some(address) = &output.address {
-            labels.insert(address.to_string(), output.address_label.clone());
-        }
-        if output.kind == UTXOKind::Deposit || output.kind == UTXOKind::Change {
-            changes_indexes.push(index);
-        }
-    }
-    labels.insert(txid, value.label);
-    let mut tx = HistoryTransaction::new(
-        value.raw,
-        value.block_height,
-        value.confirmed_at.map(|t| t as u32),
-        coins,
-        changes_indexes,
-        network,
-    );
-    tx.load_labels(&labels);
-    tx
-}
-
-fn spend_tx_from_api(
-    value: api::Psbt,
-    desc: &LianaDescriptor,
-    secp: &secp256k1::Secp256k1<impl secp256k1::Verification>,
-    network: Network,
-) -> SpendTx {
-    let mut labels = HashMap::<String, Option<String>>::new();
-    let mut coins = Vec::new();
-    for input in &value.inputs {
-        labels.insert(
-            format!("{}:{}", input.txid, input.vout),
-            input.label.clone(),
-        );
-        if input.kind == UTXOKind::Deposit || input.kind == UTXOKind::Change {
-            if let Some(c) = &input.coin {
-                coins.push(ListCoinsEntry {
-                    address: c.address.clone(),
-                    amount: c.amount,
-                    derivation_index: c.derivation_index,
-                    outpoint: c.outpoint,
-                    block_height: c.block_height,
-                    is_immature: c.is_immature,
-                    is_change: c.is_change_address,
-                    spend_info: c.spend_info.clone().map(|info| LCSpendInfo {
-                        txid: info.txid,
-                        height: info.height,
-                    }),
-                });
-            }
-        }
-    }
-    let mut changes_indexes = Vec::new();
-    let txid = value.raw.unsigned_tx.txid().to_string();
-    for (index, output) in value.outputs.iter().enumerate() {
-        labels.insert(format!("{}:{}", txid, index), output.label.clone());
-        if let Some(address) = &output.address {
-            labels.insert(address.to_string(), output.address_label.clone());
-        }
-        if output.kind == UTXOKind::Deposit || output.kind == UTXOKind::Change {
-            changes_indexes.push(index);
-        }
-    }
-    labels.insert(txid, value.label);
-    let mut tx = SpendTx::new(
-        Some(value.updated_at as u32),
-        value.raw,
-        coins,
-        desc,
-        secp,
-        network,
-    );
-    tx.load_labels(&labels);
-    tx
-}
diff --git a/gui/src/lianalite/client/mod.rs b/gui/src/lianalite/client/mod.rs
deleted file mode 100644
index b54d28d7..00000000
--- a/gui/src/lianalite/client/mod.rs
+++ /dev/null
@@ -1,32 +0,0 @@
-pub mod auth;
-pub mod backend;
-
-use liana::miniscript::bitcoin;
-
-use serde::Deserialize;
-
-const LIANALITE_SIGNET_URL: &str = "https://signet.lianalite.com";
-const LIANALITE_MAINNET_URL: &str = "https://lianalite.com";
-
-#[derive(Debug, Clone, Deserialize)]
-pub struct ServiceConfig {
-    pub auth_api_url: String,
-    pub auth_api_public_key: String,
-    pub backend_api_url: String,
-}
-
-pub async fn get_service_config(
-    network: bitcoin::Network,
-) -> Result<ServiceConfig, reqwest::Error> {
-    reqwest::get(format!(
-        "{}/api/env",
-        if network == bitcoin::Network::Bitcoin {
-            LIANALITE_MAINNET_URL
-        } else {
-            LIANALITE_SIGNET_URL
-        }
-    ))
-    .await?
-    .json()
-    .await
-}
diff --git a/gui/src/lianalite/mod.rs b/gui/src/lianalite/mod.rs
deleted file mode 100644
index 231cb2a9..00000000
--- a/gui/src/lianalite/mod.rs
+++ /dev/null
@@ -1,86 +0,0 @@
-pub mod client;
-
-use std::collections::HashMap;
-use std::fs::OpenOptions;
-use std::io::Write;
-use std::path::PathBuf;
-
-use liana::miniscript::bitcoin::Network;
-use serde::{Deserialize, Serialize};
-
-#[derive(Debug, Clone, Deserialize, Serialize)]
-pub struct Config {
-    auth: HashMap<String, NetworkAuthConfig>,
-}
-
-#[derive(Debug, Clone, Deserialize, Serialize)]
-pub struct NetworkAuthConfig {
-    email: String,
-    access_token: String,
-    expires_at: i64,
-    refresh_token: String,
-}
-
-pub const DEFAULT_FILE_NAME: &str = "lite.json";
-
-impl Config {
-    pub fn file_path(datadir: PathBuf, network: Network) -> PathBuf {
-        let mut path = datadir;
-        path.push(network.to_string());
-        path.push(DEFAULT_FILE_NAME);
-        path
-    }
-    pub fn from_file(datadir: PathBuf, network: Network) -> Result<Self, ConfigError> {
-        let path = Self::file_path(datadir, network);
-
-        let config = std::fs::read(path)
-            .map_err(|e| match e.kind() {
-                std::io::ErrorKind::NotFound => ConfigError::NotFound,
-                _ => ConfigError::ReadingFile(format!("Reading settings file: {}", e)),
-            })
-            .and_then(|file_content| {
-                serde_json::from_slice::<Config>(&file_content)
-                    .map_err(|e| ConfigError::ReadingFile(format!("Parsing settings file: {}", e)))
-            })?;
-        Ok(config)
-    }
-
-    pub fn to_file(&self, datadir: PathBuf, network: Network) -> Result<(), ConfigError> {
-        let path = Self::file_path(datadir, network);
-
-        let content = serde_json::to_string_pretty(&self).map_err(|e| {
-            ConfigError::WritingFile(format!("Failed to serialize settings: {}", e))
-        })?;
-
-        let mut settings_file = OpenOptions::new()
-            .write(true)
-            .create(true)
-            .truncate(true)
-            .open(path)
-            .map_err(|e| ConfigError::WritingFile(e.to_string()))?;
-
-        settings_file.write_all(content.as_bytes()).map_err(|e| {
-            tracing::warn!("failed to write to file: {:?}", e);
-            ConfigError::WritingFile(e.to_string())
-        })
-    }
-}
-
-#[derive(PartialEq, Eq, Debug, Clone)]
-pub enum ConfigError {
-    NotFound,
-    ReadingFile(String),
-    WritingFile(String),
-    Unexpected(String),
-}
-
-impl std::fmt::Display for ConfigError {
-    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
-        match self {
-            Self::NotFound => write!(f, "Settings file not found"),
-            Self::ReadingFile(e) => write!(f, "Error while reading file: {}", e),
-            Self::WritingFile(e) => write!(f, "Error while writing file: {}", e),
-            Self::Unexpected(e) => write!(f, "Unexpected error: {}", e),
-        }
-    }
-}
diff --git a/gui/src/lib.rs b/gui/src/lib.rs
index 915b579b..c7c5d032 100644
--- a/gui/src/lib.rs
+++ b/gui/src/lib.rs
@@ -5,7 +5,6 @@ pub mod download;
 pub mod hw;
 pub mod installer;
 pub mod launcher;
-pub mod lianalite;
 pub mod loader;
 pub mod logger;
 pub mod signer;
diff --git a/gui/src/loader.rs b/gui/src/loader.rs
index 47e22ac3..ec09d898 100644
--- a/gui/src/loader.rs
+++ b/gui/src/loader.rs
@@ -22,7 +22,6 @@ use liana_ui::{
     widget::*,
 };
 
-use crate::daemon::DaemonBackend;
 use crate::{
     app::{
         cache::Cache,
@@ -129,8 +128,8 @@ impl Loader {
                     self.step = Step::Error(Box::new(e));
                 }
                 Error::Daemon(DaemonError::ClientNotSupported)
-                | Error::Daemon(DaemonError::RpcSocket(Some(ErrorKind::ConnectionRefused), _))
-                | Error::Daemon(DaemonError::RpcSocket(Some(ErrorKind::NotFound), _)) => {
+                | Error::Daemon(DaemonError::Transport(Some(ErrorKind::ConnectionRefused), _))
+                | Error::Daemon(DaemonError::Transport(Some(ErrorKind::NotFound), _)) => {
                     if let Some(daemon_config_path) = self.gui_config.daemon_config_path.clone() {
                         self.step = Step::StartingDaemon;
                         self.daemon_started = true;
@@ -227,7 +226,7 @@ impl Loader {
     pub fn stop(&mut self) {
         info!("Close requested");
         if let Step::Syncing { daemon, .. } = &mut self.step {
-            if daemon.backend() == DaemonBackend::EmbeddedLianad {
+            if !daemon.is_external() {
                 info!("Stopping internal daemon...");
                 if let Err(e) = Handle::current().block_on(async { daemon.stop().await }) {
                     warn!("Internal daemon failed to stop: {}", e);
@@ -367,9 +366,7 @@ pub async fn load_application(
     ),
     Error,
 > {
-    let wallet = Wallet::new(info.descriptors.main)
-        .load_from_settings(&datadir_path, network)?
-        .load_hotsigners(&datadir_path, network)?;
+    let wallet = Wallet::new(info.descriptors.main).load_settings(&datadir_path, network)?;
 
     let coins = daemon
         .list_coins(&[CoinStatus::Unconfirmed, CoinStatus::Confirmed], &[])
diff --git a/gui/src/main.rs b/gui/src/main.rs
index e7f079ed..ba344fc5 100644
--- a/gui/src/main.rs
+++ b/gui/src/main.rs
@@ -1,9 +1,5 @@
 #![windows_subsystem = "windows"]
 
-use std::{
-    collections::HashMap, error::Error, io::Write, path::PathBuf, process, str::FromStr, sync::Arc,
-};
-
 use iced::{
     event::{self, Event},
     executor, keyboard,
@@ -11,6 +7,7 @@ use iced::{
     window::settings::PlatformSpecific,
     Application, Command, Settings, Size, Subscription,
 };
+use std::{error::Error, io::Write, path::PathBuf, process, str::FromStr};
 use tracing::{error, info};
 use tracing_subscriber::filter::LevelFilter;
 extern crate serde;
@@ -22,15 +19,11 @@ use liana_ui::{component::text, font, image, theme, widget::Element};
 use liana_gui::{
     app::{
         self,
-        cache::Cache,
         config::{default_datadir, ConfigError},
-        wallet::Wallet,
         App,
     },
-    hw::HardwareWalletConfig,
     installer::{self, Installer},
     launcher::{self, Launcher},
-    lianalite::client::{auth::AuthClient, backend::BackendClient, get_service_config},
     loader::{self, Loader},
     logger::Logger,
     VERSION,
@@ -41,8 +34,6 @@ enum Arg {
     ConfigPath(PathBuf),
     DatadirPath(PathBuf),
     Network(bitcoin::Network),
-    Email(String),
-    RefreshToken(String),
 }
 
 fn parse_args(args: Vec<String>) -> Result<Vec<Arg>, Box<dyn Error>> {
@@ -53,25 +44,6 @@ fn parse_args(args: Vec<String>) -> Result<Vec<Arg>, Box<dyn Error>> {
         process::exit(1);
     }
 
-    if args.len() > 1 && (args[1] == "--help" || args[1] == "-h") {
-        eprintln!(
-            r#"
-Usage: liana-gui [OPTIONS]
-
-Options:
-    --conf <PATH>       Path of configuration file (gui.toml)
-    --datadir <PATH>    Path of liana datadir
-    -v, --version       Display liana-gui version
-    -h, --help          Print help
-    --bitcoin           Use bitcoin network
-    --testnet           Use testnet network
-    --signet            Use signet network
-    --regtest           Use regtest network
-        "#
-        );
-        process::exit(1);
-    }
-
     for (i, arg) in args.iter().enumerate() {
         if arg == "--conf" {
             if let Some(a) = args.get(i + 1) {
@@ -85,18 +57,6 @@ Options:
             } else {
                 return Err("missing arg to --datadir".into());
             }
-        } else if arg == "--email" {
-            if let Some(a) = args.get(i + 1) {
-                res.push(Arg::Email(a.to_string()));
-            } else {
-                return Err("missing arg to --email".into());
-            }
-        } else if arg == "--refresh_token" {
-            if let Some(a) = args.get(i + 1) {
-                res.push(Arg::RefreshToken(a.to_string()));
-            } else {
-                return Err("missing arg to --access_token".into());
-            }
         } else if arg.contains("--") {
             let network = bitcoin::Network::from_str(args[i].trim_start_matches("--"))?;
             res.push(Arg::Network(network));
@@ -204,99 +164,6 @@ impl Application for GUI {
                 cmds.push(command.map(|msg| Message::Load(Box::new(msg))));
                 State::Loader(Box::new(loader))
             }
-            Config::RunWithRemoteBackend(email, refresh_token) => {
-                let rt = tokio::runtime::Runtime::new().unwrap();
-
-                // Spawn the root task
-                let (wallet, client) = rt.block_on(async {
-                    let config = get_service_config(bitcoin::Network::Signet).await.unwrap();
-                    let backend_url = config.backend_api_url.to_owned();
-
-                    let supabase_client =
-                        AuthClient::new(config.auth_api_url, config.auth_api_public_key);
-                    let access = match refresh_token {
-                        None => {
-                            supabase_client.sign_in_otp(&email).await.unwrap();
-
-                            eprintln!("Please enter token:");
-                            let mut token = String::new();
-                            std::io::stdin()
-                                .read_line(&mut token)
-                                .expect("Failed to read line");
-
-                            supabase_client
-                                .verify_otp(&email, token.trim_end())
-                                .await
-                                .unwrap()
-                        }
-                        Some(token) => supabase_client.refresh_token(&token).await.unwrap(),
-                    };
-
-                    let client =
-                        BackendClient::connect(supabase_client, backend_url, access.clone())
-                            .await
-                            .unwrap();
-                    let (client, wallet) = client.connect_first().await.unwrap();
-                    eprintln!(
-                        "Connected, next time connect directly without otp verification with:"
-                    );
-                    eprintln!(
-                        "cargo run -- --email {} --refresh_token {}",
-                        email, access.refresh_token
-                    );
-
-                    (wallet, client)
-                });
-                let hws: Vec<HardwareWalletConfig> = wallet
-                    .metadata
-                    .ledger_hmacs
-                    .into_iter()
-                    .map(|ledger_hmac| HardwareWalletConfig {
-                        kind: async_hwi::DeviceKind::Ledger.to_string(),
-                        fingerprint: ledger_hmac.fingerprint,
-                        token: ledger_hmac.hmac,
-                    })
-                    .collect();
-                let aliases: HashMap<bitcoin::bip32::Fingerprint, String> = wallet
-                    .metadata
-                    .fingerprint_aliases
-                    .into_iter()
-                    .filter_map(|a| {
-                        if a.user_id == client.user_id() {
-                            Some((a.fingerprint, a.alias))
-                        } else {
-                            None
-                        }
-                    })
-                    .collect();
-                let (app, command) = App::new(
-                    Cache {
-                        network: bitcoin::Network::Signet,
-                        coins: Vec::new(),
-                        rescan_progress: None,
-                        datadir_path: default_datadir().unwrap(),
-                        blockheight: wallet.tip_height.unwrap_or(0),
-                    },
-                    Arc::new(
-                        Wallet::new(wallet.descriptor)
-                            .with_name(wallet.name)
-                            .with_key_aliases(aliases)
-                            .with_hardware_wallets(hws),
-                    ),
-                    app::Config {
-                        daemon_config_path: None,
-                        daemon_rpc_path: None,
-                        log_level: None,
-                        debug: None,
-                        start_internal_bitcoind: false,
-                    },
-                    Arc::new(client),
-                    default_datadir().unwrap(),
-                    None,
-                );
-                cmds.push(command.map(|msg| Message::Run(Box::new(msg))));
-                State::App(app)
-            }
         };
         (
             Self {
@@ -494,7 +361,6 @@ pub enum Config {
     Run(PathBuf, app::Config, bitcoin::Network),
     Launcher(PathBuf),
     Install(PathBuf, bitcoin::Network),
-    RunWithRemoteBackend(String, Option<String>),
 }
 
 impl Config {
@@ -524,12 +390,6 @@ fn main() -> Result<(), Box<dyn Error>> {
             let datadir_path = default_datadir().unwrap();
             Config::new(datadir_path, None)
         }
-        [Arg::Email(email)] => Ok(Config::RunWithRemoteBackend(email.to_string(), None)),
-        [Arg::Email(email), Arg::RefreshToken(token)]
-        | [Arg::RefreshToken(token), Arg::Email(email)] => Ok(Config::RunWithRemoteBackend(
-            email.to_string(),
-            Some(token.to_string()),
-        )),
         [Arg::Network(network)] => {
             let datadir_path = default_datadir().unwrap();
             Config::new(datadir_path, Some(*network))
diff --git a/gui/ui/src/component/amount.rs b/gui/ui/src/component/amount.rs
index 172a1363..afe18af0 100644
--- a/gui/ui/src/component/amount.rs
+++ b/gui/ui/src/component/amount.rs
@@ -3,38 +3,82 @@ pub use bitcoin::Amount;
 use crate::{color, component::text::*, widget::*};
 
 pub fn amount<'a, T: 'a>(a: &Amount) -> Row<'a, T> {
-    amount_with_size(a, P1_SIZE)
+    render_amount(amount_as_string(*a), P1_SIZE)
 }
 
 pub fn amount_with_size<'a, T: 'a>(a: &Amount, size: u16) -> Row<'a, T> {
+    render_amount(amount_as_string(*a), size)
+}
+
+pub fn unconfirmed_amount_with_size<'a, T: 'a>(a: &Amount, size: u16) -> Row<'a, T> {
+    render_unconfirmed_amount(amount_as_string(*a), size)
+}
+
+//
+// Helpers
+//
+
+// Format a BTC amount as a string for display.
+fn amount_as_string(a: Amount) -> String {
+    let amount = a.to_btc().to_string();
+
+    // Reformat the integer portion of the amount with space separation.
+    let (integer, fraction) = match amount.split_once('.') {
+        Some((i, f)) => (i, f),
+        None => (amount.as_str(), "00000000"),
+    };
+
+    let integer = format_amount_number_part(integer);
+    let fraction = format_amount_number_part(&format!("{:0<8}", fraction));
+
+    format!("{integer}.{fraction}")
+}
+
+// Format a "part" of a number string with spaces to fit display requirements.
+// Currently using French formatting rules so digits are space-separated in groups
+// of three, starting from the right side. Incidentally, this works for both the
+// integer portion of the number as well as the fraction part.
+// Ex:
+//   1000 => 1 000
+//   100000 => 100 000
+fn format_amount_number_part(s: &str) -> String {
+    let mut part = s
+        .chars()
+        .collect::<Vec<_>>()
+        .rchunks(3)
+        .map(|c| c.iter().collect::<String>())
+        .collect::<Vec<_>>();
+    part.reverse();
+
+    part.join(" ")
+}
+
+// Helper functions split a string at the first occurence of a non-zero integer (where
+// the amount starts).
+fn split_at_first_non_zero(s: String) -> Option<(String, String)> {
+    for (index, c) in s.char_indices() {
+        if c.is_ascii_digit() && c != '0' {
+            let (before, after) = s.split_at(index);
+            return Some((before.to_string(), after.to_string()));
+        }
+    }
+    None
+}
+
+// Build the rendering elements for displaying a Bitcoin amount.
+// The text should be bolded beginning where the BTC amount is non-zero.
+fn render_amount<'a, T: 'a>(amount: String, size: u16) -> Row<'a, T> {
     let spacing = if size > P1_SIZE { 10 } else { 5 };
-    let sats = format!("{:.8}", a.to_btc());
-    assert!(sats.len() >= 9);
+
+    let (before, after) = match split_at_first_non_zero(amount) {
+        Some((b, a)) => (b, a),
+        None => (String::from(""), String::from("")),
+    };
+
     let row = Row::new()
         .spacing(spacing)
-        .push(split_digits(sats[0..sats.len() - 6].to_string(), size, true).into())
-        .push(if a.to_sat() < 1_000_000 {
-            split_digits(sats[sats.len() - 6..sats.len() - 3].to_string(), size, true).into()
-        } else {
-            Row::new()
-                .push(
-                    text(sats[sats.len() - 6..sats.len() - 3].to_string())
-                        .bold()
-                        .size(size),
-                )
-                .into()
-        })
-        .push(if a.to_sat() < 1000 {
-            split_digits(sats[sats.len() - 3..sats.len()].to_string(), size, true).into()
-        } else {
-            Row::new()
-                .push(
-                    text(sats[sats.len() - 3..sats.len()].to_string())
-                        .bold()
-                        .size(size),
-                )
-                .into()
-        });
+        .push(text(before).size(size).style(color::GREY_3))
+        .push(text(after).size(size).bold());
 
     Row::with_children(vec![
         row.into(),
@@ -44,32 +88,13 @@ pub fn amount_with_size<'a, T: 'a>(a: &Amount, size: u16) -> Row<'a, T> {
     .align_items(iced::Alignment::Center)
 }
 
-pub fn unconfirmed_amount_with_size<'a, T: 'a>(a: &Amount, size: u16) -> Row<'a, T> {
+// Build the rendering elements for displaying a Bitcoin amount.
+fn render_unconfirmed_amount<'a, T: 'a>(amount: String, size: u16) -> Row<'a, T> {
     let spacing = if size > P1_SIZE { 10 } else { 5 };
-    let sats = format!("{:.8}", a.to_btc());
-    assert!(sats.len() >= 9);
+
     let row = Row::new()
         .spacing(spacing)
-        .push(split_digits(sats[0..sats.len() - 6].to_string(), size, false).into())
-        .push(if a.to_sat() < 1_000_000 {
-            split_digits(
-                sats[sats.len() - 6..sats.len() - 3].to_string(),
-                size,
-                false,
-            )
-            .into()
-        } else {
-            Row::new()
-                .push(text(sats[sats.len() - 6..sats.len() - 3].to_string()).size(size))
-                .into()
-        })
-        .push(if a.to_sat() < 1000 {
-            split_digits(sats[sats.len() - 3..sats.len()].to_string(), size, false).into()
-        } else {
-            Row::new()
-                .push(text(sats[sats.len() - 3..sats.len()].to_string()).size(size))
-                .into()
-        });
+        .push(text(amount).size(size).style(color::GREY_3));
 
     Row::with_children(vec![
         row.into(),
@@ -79,25 +104,23 @@ pub fn unconfirmed_amount_with_size<'a, T: 'a>(a: &Amount, size: u16) -> Row<'a,
     .align_items(iced::Alignment::Center)
 }
 
-fn split_digits<'a, T: 'a>(mut s: String, size: u16, bold: bool) -> impl Into<Element<'a, T>> {
-    let prefixes = vec!["0.00", "0.0", "0.", "000", "00", "0"];
-    for prefix in prefixes {
-        if s.starts_with(prefix) {
-            let right = s.split_off(prefix.len());
-            return Row::new()
-                .push(text(s).size(size).style(color::GREY_3))
-                .push_maybe(if right.is_empty() {
-                    None
-                } else if bold {
-                    Some(text(right).bold().size(size))
-                } else {
-                    Some(text(right).size(size))
-                });
-        }
-    }
-    if bold {
-        Row::new().push(text(s).bold().size(size))
-    } else {
-        Row::new().push(text(s).size(size))
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_amount_as_str() {
+        assert_eq!(
+            "0.00 799 800",
+            amount_as_string(bitcoin::Amount::from_btc(0.00799800).unwrap())
+        );
+        assert_eq!(
+            "1 000.00 799 800",
+            amount_as_string(bitcoin::Amount::from_btc(1000.00799800).unwrap())
+        );
+        assert_eq!(
+            "1 000.00 000 000",
+            amount_as_string(bitcoin::Amount::from_btc(1000.0).unwrap())
+        );
     }
 }
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
index 276758db..714cd1b3 100644
--- a/src/commands/mod.rs
+++ b/src/commands/mod.rs
@@ -8,11 +8,10 @@ use crate::{
     bitcoin::BitcoinInterface,
     database::{Coin, DatabaseConnection, DatabaseInterface},
     descriptors,
-    miniscript::bitcoin::absolute::LockTime,
     poller::PollerMessage,
     spend::{
-        self, create_spend, AddrInfo, AncestorInfo, CandidateCoin, CreateSpendRes,
-        SpendCreationError, SpendOutputAddress, SpendTxFees, TxGetter,
+        create_spend, AddrInfo, AncestorInfo, CandidateCoin, CreateSpendRes, SpendCreationError,
+        SpendOutputAddress, SpendTxFees, TxGetter,
     },
     DaemonControl, VERSION,
 };
@@ -26,10 +25,8 @@ use utils::{
 
 use std::{
     collections::{hash_map, HashMap, HashSet},
-    convert::TryInto,
     fmt,
     sync::{self, mpsc},
-    time::SystemTime,
 };
 
 use miniscript::{
@@ -134,7 +131,7 @@ impl From<SpendCreationError> for CommandError {
 pub enum RbfErrorInfo {
     MissingFeerate,
     SuperfluousFeerate,
-    TooLowFeerate(u64, u64),
+    TooLowFeerate(u64),
     NotSignaling,
 }
 
@@ -147,9 +144,7 @@ impl fmt::Display for RbfErrorInfo {
             Self::SuperfluousFeerate => {
                 write!(f, "A feerate must not be provided if creating a cancel. We'll always use the smallest one which satisfies the RBF rules.")
             }
-            Self::TooLowFeerate(r, m) => {
-                write!(f, "Feerate {} too low for minimum feerate {}.", r, m)
-            }
+            Self::TooLowFeerate(r) => write!(f, "Feerate too low: {}.", r),
             Self::NotSignaling => write!(f, "Replacement candidate does not signal for RBF."),
         }
     }
@@ -282,21 +277,6 @@ impl DaemonControl {
             }
         }
     }
-
-    // Pass relevant values to the spend module function of same name.
-    fn anti_fee_sniping_locktime(&self) -> LockTime {
-        let now = SystemTime::now()
-            .duration_since(SystemTime::UNIX_EPOCH)
-            .expect("time measured now cannot be before unix epoch");
-        let tip_time = self.bitcoin.tip_time();
-        let tip_height: u32 = self
-            .bitcoin
-            .chain_tip()
-            .height
-            .try_into()
-            .expect("block height must fit in u32");
-        spend::anti_fee_sniping_locktime(now, tip_height, tip_time)
-    }
 }
 
 impl DaemonControl {
@@ -549,7 +529,6 @@ impl DaemonControl {
         // derivation index in case any address in the transaction outputs was ours and from the
         // future.
         let change_info = change_address.info;
-        let locktime = self.anti_fee_sniping_locktime();
         let CreateSpendRes {
             psbt,
             has_change,
@@ -562,7 +541,6 @@ impl DaemonControl {
             &candidate_coins,
             SpendTxFees::Regular(feerate_vb),
             change_address,
-            locktime,
         ) {
             Ok(res) => res,
             Err(SpendCreationError::CoinSelection(e)) => {
@@ -825,7 +803,6 @@ impl DaemonControl {
         if feerate_vb < min_feerate_vb {
             return Err(CommandError::RbfError(RbfErrorInfo::TooLowFeerate(
                 feerate_vb,
-                min_feerate_vb,
             )));
         }
         // Get info about prev outputs to determine replacement outputs.
@@ -924,7 +901,6 @@ impl DaemonControl {
         // will ensure that the replacement transaction additionally pays for its own weight as per
         // RBF rule 4.
         let replaced_fee = descendant_fees.to_sat();
-        let locktime = self.anti_fee_sniping_locktime();
         // This loop can have up to 2 iterations in the case of cancel and otherwise only 1.
         loop {
             match create_spend(
@@ -935,7 +911,6 @@ impl DaemonControl {
                 &candidate_coins,
                 SpendTxFees::Rbf(feerate_vb, replaced_fee),
                 change_address.clone(),
-                locktime,
             ) {
                 Ok(CreateSpendRes {
                     psbt,
@@ -1103,7 +1078,6 @@ impl DaemonControl {
         }
 
         let sweep_addr_info = sweep_addr.info;
-        let locktime = self.anti_fee_sniping_locktime();
         let CreateSpendRes {
             psbt, has_change, ..
         } = create_spend(
@@ -1114,7 +1088,6 @@ impl DaemonControl {
             &sweepable_coins,
             SpendTxFees::Regular(feerate_vb),
             sweep_addr,
-            locktime,
         )?;
         if has_change {
             self.maybe_increase_next_deriv_index(&mut db_conn, &sweep_addr_info);
diff --git a/src/spend.rs b/src/spend.rs
index 9465b775..ef858801 100644
--- a/src/spend.rs
+++ b/src/spend.rs
@@ -4,7 +4,6 @@ use std::{
     collections::{BTreeMap, HashMap},
     convert::TryInto,
     fmt,
-    time::Duration,
 };
 
 pub use bdk_coin_select::InsufficientFunds;
@@ -35,10 +34,6 @@ pub const MAX_FEE: bitcoin::Amount = bitcoin::Amount::ONE_BTC;
 /// Assume that paying more than 1000sat/vb in feerate is a bug.
 pub const MAX_FEERATE: u64 = 1_000;
 
-/// Do not set locktime if tip age in seconds is older than this.
-// See also https://github.com/bitcoin/bitcoin/blob/ecd23656db174adef61d3bd753d02698c3528192/src/wallet/spend.cpp#L906.
-pub const MAX_ANTI_FEE_SNIPING_TIP_AGE_SECS: u64 = 8 * 60 * 60; // 8 hours
-
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum InsaneFeeInfo {
     NegativeFee,
@@ -503,40 +498,6 @@ fn derived_desc(
     desc.derive(coin.deriv_index, secp)
 }
 
-/// Get value to use for transaction nLockTime in order to
-/// discourage fee sniping.
-///
-/// The approach follows that taken by Bitcoin Core:
-/// - most of the time, the value returned will be the current
-/// block height, but will randomly be up to 100 blocks earlier.
-/// - if the current tip is more than [`MAX_ANTI_FEE_SNIPING_TIP_AGE_SECS`]
-/// seconds old, a locktime value of 0 will be returned.
-pub fn anti_fee_sniping_locktime(
-    now: Duration,
-    tip_height: u32,
-    tip_time_secs: Option<u32>,
-) -> LockTime {
-    tip_time_secs
-        .map(|tip_time| now.as_secs().saturating_sub(tip_time.into()))
-        .filter(|tip_age| *tip_age <= MAX_ANTI_FEE_SNIPING_TIP_AGE_SECS)
-        .map(|_| {
-            // Randomly (approx 10% of cases) set locktime further back
-            // using current time as source of randomness.
-            let nanos = now.subsec_nanos();
-            // Note this condition will fail if nano precision is not available
-            // and so nothing will be subtracted.
-            let delta = if nanos % 10 == 1 {
-                (nanos % 1000) / 10 + 1 // a number in [1, 100]
-            } else {
-                0
-            };
-            let height = tip_height.saturating_sub(delta);
-            LockTime::from_height(height)
-                .expect("height is valid block height as it cannot be bigger than tip height")
-        })
-        .unwrap_or(LockTime::Blocks(Height::ZERO))
-}
-
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub struct AddrInfo {
     pub index: bip32::ChildNumber,
@@ -629,8 +590,6 @@ pub struct CreateSpendRes {
 /// * `change_addr`: the address to use for a change output if we need to create one. Can be set to
 /// an external address (if combined with an empty list of `destinations` it's useful to sweep some
 /// or all coins of a wallet to an external address).
-/// * `locktime`: the locktime to use for the transaction.
-#[allow(clippy::too_many_arguments)]
 pub fn create_spend(
     main_descriptor: &descriptors::LianaDescriptor,
     secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
@@ -639,7 +598,6 @@ pub fn create_spend(
     candidate_coins: &[CandidateCoin],
     fees: SpendTxFees,
     change_addr: SpendOutputAddress,
-    locktime: LockTime,
 ) -> Result<CreateSpendRes, SpendCreationError> {
     // This method does quite a few things. In addition, we support different modes (coin control
     // vs automated coin selection, self-spend, sweep, etc..) which make the logic a bit more
@@ -668,7 +626,7 @@ pub fn create_spend(
     // Create transaction with no inputs and no outputs.
     let mut tx = bitcoin::Transaction {
         version: bitcoin::transaction::Version::TWO,
-        lock_time: locktime,
+        lock_time: LockTime::Blocks(Height::ZERO), // TODO: randomized anti fee sniping
         input: Vec::with_capacity(candidate_coins.iter().filter(|c| c.must_select).count()),
         output: Vec::with_capacity(destinations.len()),
     };
@@ -830,122 +788,3 @@ pub fn create_spend(
         warnings,
     })
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    use std::time::Duration;
-
-    use miniscript::bitcoin::absolute::{Height, LockTime};
-
-    #[test]
-    fn test_anti_fee_sniping_locktime() {
-        // If we have no tip time, locktime is 0.
-        assert_eq!(
-            anti_fee_sniping_locktime(Duration::from_secs(100), 123_456, None),
-            LockTime::Blocks(Height::ZERO)
-        );
-
-        // If tip time is too old, locktime is 0.
-        assert_eq!(
-            anti_fee_sniping_locktime(
-                Duration::from_secs(100_000),
-                123_456,
-                Some(100_000 - (8 * 60 * 60) - 1)
-            ),
-            LockTime::Blocks(Height::ZERO)
-        );
-
-        // If tip age is exactly the max threshold, we set locktime.
-        assert_eq!(
-            anti_fee_sniping_locktime(
-                Duration::from_secs(100_000),
-                123_456,
-                Some(100_000 - (8 * 60 * 60))
-            ),
-            LockTime::from_height(123_456).unwrap()
-        );
-
-        // If tip time is later than now, we set locktime depending on nanos.
-        // If nanos are 0, set to current height.
-        assert_eq!(
-            anti_fee_sniping_locktime(Duration::from_secs(50_000), 123_456, Some(100_000)),
-            LockTime::from_height(123_456).unwrap()
-        );
-
-        // We might set locktime earlier than current height.
-        assert_eq!(
-            anti_fee_sniping_locktime(
-                Duration::from_secs(50_000) + Duration::from_nanos(1),
-                123_456,
-                Some(100_000)
-            ),
-            LockTime::from_height(123_455).unwrap() // subtract 1
-        );
-
-        // If tip time is older than now, we also vary the locktime depending on current nanos.
-        // If nanos are truncated or 0, set locktime to current height.
-        assert_eq!(
-            anti_fee_sniping_locktime(Duration::from_secs(100), 123_456, Some(100)),
-            LockTime::from_height(123_456).unwrap() // subtract 1
-        );
-
-        assert_eq!(
-            anti_fee_sniping_locktime(
-                Duration::from_secs(100) + Duration::from_nanos(1),
-                123_456,
-                Some(100)
-            ),
-            LockTime::from_height(123_455).unwrap() // subtract 1
-        );
-
-        assert_eq!(
-            anti_fee_sniping_locktime(
-                Duration::from_secs(100) + Duration::from_nanos(10_000_041),
-                123_456,
-                Some(100)
-            ),
-            LockTime::from_height(123_451).unwrap() // subtract 5
-        );
-
-        // If nanos % 10 != 1, don't subtract anything.
-        assert_eq!(
-            anti_fee_sniping_locktime(
-                Duration::from_secs(100) + Duration::from_nanos(10_000_040),
-                123_456,
-                Some(100)
-            ),
-            LockTime::from_height(123_456).unwrap() // subtract 0
-        );
-
-        assert_eq!(
-            anti_fee_sniping_locktime(
-                Duration::from_secs(100) + Duration::from_nanos(100_000_891),
-                123_456,
-                Some(100)
-            ),
-            LockTime::from_height(123_366).unwrap() // subtract 90
-        );
-
-        // We would subtract 90, but current height is 56, so return locktime of 0.
-        assert_eq!(
-            anti_fee_sniping_locktime(
-                Duration::from_secs(100) + Duration::from_nanos(100_000_891),
-                56,
-                Some(100)
-            ),
-            LockTime::Blocks(Height::ZERO)
-        );
-
-        // If block height is 91, we can now subtract 90.
-        assert_eq!(
-            anti_fee_sniping_locktime(
-                Duration::from_secs(100) + Duration::from_nanos(100_000_891),
-                91,
-                Some(100)
-            ),
-            LockTime::from_height(1).unwrap() // subtract 90
-        );
-    }
-}


Test patch:
diff --git a/src/testutils.rs b/src/testutils.rs
index 0d4b028e..712d3906 100644
--- a/src/testutils.rs
+++ b/src/testutils.rs
@@ -114,7 +114,7 @@ impl BitcoinInterface for DummyBitcoind {
     }
 
     fn tip_time(&self) -> Option<u32> {
-        None
+        todo!()
     }
 
     fn wallet_transaction(
diff --git a/tests/test_chain.py b/tests/test_chain.py
index 26487c99..0dcf2b49 100644
--- a/tests/test_chain.py
+++ b/tests/test_chain.py
@@ -150,8 +150,6 @@ def test_reorg_status_recovery(lianad, bitcoind):
     """
     list_coins = lambda: lianad.rpc.listcoins()["coins"]
 
-    # Generate blocks in order to test locktime set correctly.
-    bitcoind.generate_block(200)
     # Create two confirmed coins. Note how we take the initial_height after having
     # mined them, as we'll reorg back to this height and due to anti fee-sniping
     # these deposit transactions might not be valid anymore!
@@ -159,37 +157,28 @@ def test_reorg_status_recovery(lianad, bitcoind):
     txids = [bitcoind.rpc.sendtoaddress(addr, 0.5670) for addr in addresses]
     bitcoind.generate_block(1, wait_for_mempool=txids)
     initial_height = bitcoind.rpc.getblockcount()
-    assert initial_height > 100
     wait_for(lambda: lianad.rpc.getinfo()["block_height"] == initial_height)
 
     # Both coins are confirmed. Spend the second one then get their infos.
     wait_for(lambda: len(list_coins()) == 2)
     wait_for(lambda: all(c["block_height"] is not None for c in list_coins()))
     coin_b = get_coin(lianad, txids[1])
-    tx = spend_coins(lianad, bitcoind, [coin_b])
-    locktime = bitcoind.rpc.decoderawtransaction(tx)["locktime"]
-    assert initial_height - 100 <= locktime <= initial_height
+    spend_coins(lianad, bitcoind, [coin_b])
     bitcoind.generate_block(1, wait_for_mempool=1)
     wait_for(lambda: spend_confirmed_noticed(lianad, coin_b["outpoint"]))
     coin_a = get_coin(lianad, txids[0])
     coin_b = get_coin(lianad, txids[1])
 
     # Reorg the chain down to the initial height without shifting nor malleating
-    # any transaction. The coin info should be identical (except the spend info
-    # of the transaction spending the second coin).
+    # any transaction. The coin info should be identical (except the transaction
+    # spending the second coin will be mined at the height the reorg happened).
     bitcoind.simple_reorg(initial_height, shift=0)
     new_height = bitcoind.rpc.getblockcount()
     wait_for(lambda: lianad.rpc.getinfo()["block_height"] == new_height)
     new_coin_a = get_coin(lianad, coin_a["outpoint"])
     assert coin_a == new_coin_a
     new_coin_b = get_coin(lianad, coin_b["outpoint"])
-
-    if locktime == initial_height:
-        # Cannot be mined until next block (initial_height + 1).
-        coin_b["spend_info"] = None
-    else:
-        # Otherwise, the tx will be mined at the height the reorg happened.
-        coin_b["spend_info"]["height"] = initial_height
+    coin_b["spend_info"]["height"] = initial_height
     assert new_coin_b == coin_b
 
 
diff --git a/tests/test_rpc.py b/tests/test_rpc.py
index 675f8001..38894620 100644
--- a/tests/test_rpc.py
+++ b/tests/test_rpc.py
@@ -872,8 +872,6 @@ def test_listtransactions(lianad, bitcoind):
 
 def test_create_recovery(lianad, bitcoind):
     """Test the sweep of coins that are available through the timelocked path."""
-    # Generate blocks in order to test locktime set correctly.
-    bitcoind.generate_block(200)
     # Start by getting a few coins
     destinations = {
         lianad.rpc.getnewaddress()["address"]: 0.1,
@@ -902,13 +900,6 @@ def test_create_recovery(lianad, bitcoind):
     # Now we can create a recovery tx that sweeps the first 3 coins.
     res = lianad.rpc.createrecovery(bitcoind.rpc.getnewaddress(), 18)
     reco_psbt = PSBT.from_base64(res["psbt"])
-
-    # Check locktime being set correctly.
-    tip_height = bitcoind.rpc.getblockcount()
-    assert tip_height > 100
-    locktime = reco_psbt.tx.nLockTime
-    assert tip_height - 100 <= locktime <= tip_height
-
     assert len(reco_psbt.tx.vin) == 3, "The last coin's timelock hasn't matured yet"
     assert len(reco_psbt.tx.vout) == 1
     assert int(0.5999 * COIN) < int(reco_psbt.tx.vout[0].nValue) < int(0.6 * COIN)
@@ -1057,8 +1048,6 @@ def test_labels(lianad, bitcoind):
 def test_rbfpsbt_bump_fee(lianad, bitcoind):
     """Test the use of RBF to bump the fee of a transaction."""
 
-    # Generate blocks in order to test locktime set correctly.
-    bitcoind.generate_block(200)
     # Get three coins.
     destinations = {
         lianad.rpc.getnewaddress()["address"]: 0.003,
@@ -1104,19 +1093,13 @@ def test_rbfpsbt_bump_fee(lianad, bitcoind):
         )
     )
     # We can now use RBF, but the feerate must be higher than that of the first transaction.
-    with pytest.raises(RpcError, match=f"Feerate 1 too low for minimum feerate 2."):
+    with pytest.raises(RpcError, match=f"Feerate too low: 1."):
         lianad.rpc.rbfpsbt(first_txid, False, 1)
     # Using a higher feerate works.
     lianad.rpc.rbfpsbt(first_txid, False, 2)
     # Let's use an even higher feerate.
     rbf_1_res = lianad.rpc.rbfpsbt(first_txid, False, 10)
     rbf_1_psbt = PSBT.from_base64(rbf_1_res["psbt"])
-
-    # Check the locktime is being set.
-    tip_height = bitcoind.rpc.getblockcount()
-    locktime = rbf_1_psbt.tx.nLockTime
-    assert tip_height - 100 <= locktime <= tip_height
-
     # The inputs are the same in both (no new inputs needed in the replacement).
     assert sorted(i.prevout.serialize() for i in first_psbt.tx.vin) == sorted(
         i.prevout.serialize() for i in rbf_1_psbt.tx.vin
@@ -1144,9 +1127,7 @@ def test_rbfpsbt_bump_fee(lianad, bitcoind):
     # If we try to RBF the first transaction again, it will use the first RBF's
     # feerate to set the min feerate, instead of 1 sat/vb of first
     # transaction:
-    with pytest.raises(
-        RpcError, match=f"Feerate {int(rbf_1_feerate)} too low for minimum feerate 10."
-    ):
+    with pytest.raises(RpcError, match=f"Feerate too low: {int(rbf_1_feerate)}."):
         lianad.rpc.rbfpsbt(first_txid, False, int(rbf_1_feerate))
     # Using 1 more for feerate works.
     feerate = int(rbf_1_feerate) + 1