Repository: wizardsardine/liana
Pull request #289 (2023-01-18 10:41:13)

Gui multisig

darosior: nit: in the installer for the keys "Edit" button should be "Fill in" (or "Set") when it's empty, and only "Edit" after it's been set.
nit: "Name is a duplicate" -> "Duplicate alias"; nit: "Key is a duplicate" -> "Duplicate key";

In "participate in a new wallet", we should make it clear that they can click multiple times on the device button to generate more keys.

Not for this PR but in "register descriptor on signing device", we should really make it clear they *need* to click on the signing devices tabs. We could also have a checkbox here (like in descriptor backup).

In the Spend transaction details, i'm not a fan of the display of the signatures. I think it could and should be greatly simplified. We want to display three pieces of information for each spending path: the number of signatures provided, the number of signatures required and what keys can be used to provide signatures.
First of all, the information for each path should each be on its own line. Then we can reuse what you did here and in revault-gui for the keys available, that is a number of grey icons next to each other, one for each key, that turn green when a signature was provided for them. I like also that you get the alias when hovering the icon. Maybe the alias could even be made a first class citizen here. Finally, the required number of keys can simply be within a sentence. Here is an instance of what i'm thinking about with a 3-of-3 between Alice, Bob and Carol that turns into a 1-of-3 between Alice, Bob and Dave:
- 3 signatures out of `Alice`, `Bob` and `Carol`; or
- 1 signature out of `Alice`, `Bob` and `Dave`
 
Note how this generalizes well to multiple paths. For instance if we add a third path in between the two it just becomes:
- 3 signatures out of `Alice`, `Bob` and `Carol`; or
- 2 signatures out of `Alice`, `Bob` and `Carol`
- 1 signature out of `Alice`, `Bob` and `Dave`

Ideally, i think we should also optimize out the very common cases. For instance, a single key is used and a single path is available: in this case just display whether the transaction is signed or not.

Patch:
diff --git a/contrib/reproducible/guix/build.sh b/contrib/reproducible/guix/build.sh
index d8d6535d..17cd8731 100755
--- a/contrib/reproducible/guix/build.sh
+++ b/contrib/reproducible/guix/build.sh
@@ -35,19 +35,11 @@ RUSTC_BOOTSTRAP=1 cargo -vvv \
     --release \
     --target-dir "/out"
 
-if [ "$IS_GUI" = "1" ]; then
-    BIN_NAMES="liana-gui"
-else
-    BIN_NAMES="lianad liana-cli"
-fi
-
-for bin_name in $BIN_NAMES; do
-    # Assume 64bits. Even bitcoind doesn't ship 32bits binaries for x86.
-    # FIXME: is there a cleaner way than using patchelf for this?
-    patchelf --set-interpreter /lib64/ld-linux-x86-64.so.2 "/out/release/$bin_name"
-
-    # FIXME: Find a way to use GUIX_LD_WRAPPER_DISABLE_RPATH=yes instead
-    patchelf --remove-rpath "/out/release/$bin_name"
-done
+# Assume 64bits. Even bitcoind doesn't ship 32bits binaries for x86.
+# FIXME: is there a cleaner way than using patchelf for this?
+patchelf --set-interpreter /lib64/ld-linux-x86-64.so.2 "/out/release/$BINARY_NAME"
+
+# FIXME: Find a way to use GUIX_LD_WRAPPER_DISABLE_RPATH=yes instead
+patchelf --remove-rpath "/out/release/$BINARY_NAME"
 
 set +ex
diff --git a/contrib/reproducible/guix/guix-build.sh b/contrib/reproducible/guix/guix-build.sh
index 128cbcc6..d08687b5 100755
--- a/contrib/reproducible/guix/guix-build.sh
+++ b/contrib/reproducible/guix/guix-build.sh
@@ -104,9 +104,9 @@ for project_folder in "" "gui"; do
     # FIXME: find a cleaner way to get the binary name, or get rid of patchelf entirely
     # Note: we also rely on it in manifest.scm
     if [ "$project_folder" = "" ]; then
-        IS_GUI=0
+        BINARY_NAME="lianad"
     elif [ "$project_folder" = "gui" ]; then
-        IS_GUI=1
+        BINARY_NAME="liana-gui"
     else
         echo "Can't determine binary name"
         exit 1
@@ -114,10 +114,10 @@ for project_folder in "" "gui"; do
 
     # Bootstrap a reproducible environment as specified by the manifest in an isolated
     # container, and build the project.
-    # NOTE: it looks like "--rebuild-cache" is necessary for the IS_GUI variable to
+    # NOTE: it looks like "--rebuild-cache" is necessary for the BINARY_NAME variable to
     # be taken into account when building the container (otherwise the GUI container could
     # miss some dependencies).
-    IS_GUI="$IS_GUI" time_machine shell --no-cwd \
+    BINARY_NAME="$BINARY_NAME" time_machine shell --no-cwd \
                --expose="$PROJECT_ROOT/src=/liana/src" \
                --expose="$PWD/gui/static=/liana/static" \
                --expose="$PROJECT_ROOT/Cargo.toml=/liana/Cargo.toml" \
@@ -131,7 +131,7 @@ for project_folder in "" "gui"; do
                --fallback \
                --rebuild-cache \
                -m $PWD/contrib/reproducible/guix/manifest.scm \
-               -- env CC=gcc VENDOR_DIR="$PROJECT_VENDOR_DIR" TARGET_DIR="$PROJECT_OUT_DIR" IS_GUI="$IS_GUI" JOBS="$JOBS" \
+               -- env CC=gcc VENDOR_DIR="$PROJECT_VENDOR_DIR" TARGET_DIR="$PROJECT_OUT_DIR" BINARY_NAME="$BINARY_NAME" JOBS="$JOBS" \
                   /bin/sh -c "cd /liana && ./build.sh"
 done
 
diff --git a/contrib/reproducible/guix/manifest.scm b/contrib/reproducible/guix/manifest.scm
index 8bba3395..5ea7a81a 100644
--- a/contrib/reproducible/guix/manifest.scm
+++ b/contrib/reproducible/guix/manifest.scm
@@ -6,9 +6,9 @@
           "patchelf"
           "gcc-toolchain@10.3.0")
           ;; Additional dependencies for building the GUI
-          (let ((is_gui (getenv "IS_GUI")))
+          (let ((binary_name (getenv "BINARY_NAME")))
             (if
-              (string=? is_gui "1")
+              (string=? binary_name "liana-gui")
               (list "pkg-config"
                     "eudev"
                     "fontconfig")
diff --git a/gui/Cargo.lock b/gui/Cargo.lock
index d93a447a..637f78b1 100644
--- a/gui/Cargo.lock
+++ b/gui/Cargo.lock
@@ -1640,7 +1640,7 @@ dependencies = [
 [[package]]
 name = "liana"
 version = "0.1.0"
-source = "git+https://github.com/revault/liana?branch=master#863cea55d7d84ea2262a68dcd006393a6ee239a4"
+source = "git+https://github.com/wizardsardine/liana?branch=master#f433002e91b09ab700d06026e1d94c462aca9756"
 dependencies = [
  "backtrace",
  "base64",
diff --git a/gui/Cargo.toml b/gui/Cargo.toml
index c738f62f..6f0f16c5 100644
--- a/gui/Cargo.toml
+++ b/gui/Cargo.toml
@@ -15,7 +15,7 @@ path = "src/main.rs"
 
 [dependencies]
 async-hwi = "0.0.2"
-liana = { git = "https://github.com/revault/liana", branch = "master", default-features = false }
+liana = { git = "https://github.com/wizardsardine/liana", branch = "master", default-features = false }
 backtrace = "0.3"
 base64 = "0.13"
 
diff --git a/gui/src/app/cache.rs b/gui/src/app/cache.rs
index e75429fc..4c13ed10 100644
--- a/gui/src/app/cache.rs
+++ b/gui/src/app/cache.rs
@@ -1,6 +1,7 @@
 use crate::daemon::model::{Coin, SpendTx};
 use liana::miniscript::bitcoin::Network;
 
+#[derive(Debug)]
 pub struct Cache {
     pub network: Network,
     pub blockheight: i32,
diff --git a/gui/src/app/config.rs b/gui/src/app/config.rs
index 8cf30fc1..459de2d7 100644
--- a/gui/src/app/config.rs
+++ b/gui/src/app/config.rs
@@ -11,19 +11,19 @@ pub struct Config {
     /// Use iced debug feature if true.
     pub debug: Option<bool>,
     /// hardware wallets config.
-    #[serde(default)]
-    pub hardware_wallets: Vec<HardwareWalletConfig>,
+    /// LEGACY: Use Settings module instead.
+    pub hardware_wallets: Option<Vec<HardwareWalletConfig>>,
 }
 
 pub const DEFAULT_FILE_NAME: &str = "gui.toml";
 
 impl Config {
-    pub fn new(daemon_config_path: PathBuf, hardware_wallets: Vec<HardwareWalletConfig>) -> Self {
+    pub fn new(daemon_config_path: PathBuf) -> Self {
         Self {
             daemon_config_path,
             log_level: None,
             debug: None,
-            hardware_wallets,
+            hardware_wallets: None,
         }
     }
 
@@ -40,14 +40,6 @@ impl Config {
             })?;
         Ok(config)
     }
-
-    pub fn default_path() -> Result<PathBuf, ConfigError> {
-        let mut datadir = default_datadir().map_err(|_| {
-            ConfigError::Unexpected("Could not locate the default datadir directory.".to_owned())
-        })?;
-        datadir.push(DEFAULT_FILE_NAME);
-        Ok(datadir)
-    }
 }
 
 #[derive(PartialEq, Eq, Debug, Clone)]
diff --git a/gui/src/app/message.rs b/gui/src/app/message.rs
index c06e4b84..bffe8e7c 100644
--- a/gui/src/app/message.rs
+++ b/gui/src/app/message.rs
@@ -23,6 +23,7 @@ pub enum Message {
     Coins(Result<Vec<Coin>, Error>),
     SpendTxs(Result<Vec<SpendTx>, Error>),
     Psbt(Result<Psbt, Error>),
+    Recovery(Result<SpendTx, Error>),
     Signed(Result<(Psbt, Fingerprint), Error>),
     Updated(Result<(), Error>),
     Saved(Result<(), Error>),
diff --git a/gui/src/app/mod.rs b/gui/src/app/mod.rs
index 17b08751..acab0d48 100644
--- a/gui/src/app/mod.rs
+++ b/gui/src/app/mod.rs
@@ -2,6 +2,7 @@ pub mod cache;
 pub mod config;
 pub mod menu;
 pub mod message;
+pub mod settings;
 pub mod state;
 pub mod view;
 pub mod wallet;
@@ -31,14 +32,14 @@ pub struct App {
     state: Box<dyn State>,
     cache: Cache,
     config: Config,
-    wallet: Wallet,
+    wallet: Arc<Wallet>,
     daemon: Arc<dyn Daemon + Sync + Send>,
 }
 
 impl App {
     pub fn new(
         cache: Cache,
-        wallet: Wallet,
+        wallet: Arc<Wallet>,
         config: Config,
         daemon: Arc<dyn Daemon + Sync + Send>,
     ) -> (App, Command<Message>) {
@@ -72,22 +73,15 @@ impl App {
             .into(),
             menu::Menu::Recovery => RecoveryPanel::new(
                 self.wallet.clone(),
-                self.config.clone(),
                 &self.cache.coins,
                 self.wallet.main_descriptor.timelock_value(),
                 self.cache.blockheight as u32,
             )
             .into(),
             menu::Menu::Receive => ReceivePanel::default().into(),
-            menu::Menu::Spend => SpendPanel::new(
-                self.wallet.clone(),
-                self.config.clone(),
-                &self.cache.spend_txs,
-            )
-            .into(),
+            menu::Menu::Spend => SpendPanel::new(self.wallet.clone(), &self.cache.spend_txs).into(),
             menu::Menu::CreateSpendTx => CreateSpendPanel::new(
                 self.wallet.clone(),
-                self.config.clone(),
                 &self.cache.coins,
                 self.cache.blockheight as u32,
             )
diff --git a/gui/src/app/settings.rs b/gui/src/app/settings.rs
new file mode 100644
index 00000000..3f917c92
--- /dev/null
+++ b/gui/src/app/settings.rs
@@ -0,0 +1,75 @@
+use std::collections::HashMap;
+use std::path::Path;
+
+use liana::miniscript::bitcoin::util::bip32::Fingerprint;
+use serde::{Deserialize, Serialize};
+
+use crate::hw::HardwareWalletConfig;
+
+///! Settings is the module to handle the GUI settings file.
+///! The settings file is used by the GUI to store useful information.
+pub const DEFAULT_FILE_NAME: &str = "settings.json";
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct Settings {
+    pub wallets: Vec<WalletSetting>,
+}
+
+impl Settings {
+    pub fn from_file(path: &Path) -> Result<Self, SettingsError> {
+        let config = std::fs::read(path)
+            .map_err(|e| match e.kind() {
+                std::io::ErrorKind::NotFound => SettingsError::NotFound,
+                _ => SettingsError::ReadingFile(format!("Reading settings file: {}", e)),
+            })
+            .and_then(|file_content| {
+                serde_json::from_slice::<Settings>(&file_content).map_err(|e| {
+                    SettingsError::ReadingFile(format!("Parsing settings file: {}", e))
+                })
+            })?;
+        Ok(config)
+    }
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct WalletSetting {
+    pub name: String,
+    pub descriptor_checksum: String,
+    #[serde(default)]
+    pub keys: Vec<KeySetting>,
+    #[serde(default)]
+    pub hardware_wallets: Vec<HardwareWalletConfig>,
+}
+
+impl WalletSetting {
+    pub fn keys_aliases(&self) -> HashMap<Fingerprint, String> {
+        let mut map = HashMap::new();
+        for key in self.keys.clone() {
+            map.insert(key.master_fingerprint, key.name);
+        }
+        map
+    }
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct KeySetting {
+    pub name: String,
+    pub master_fingerprint: Fingerprint,
+}
+
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub enum SettingsError {
+    NotFound,
+    ReadingFile(String),
+    Unexpected(String),
+}
+
+impl std::fmt::Display for SettingsError {
+    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::Unexpected(e) => write!(f, "Unexpected error: {}", e),
+        }
+    }
+}
diff --git a/gui/src/app/state/mod.rs b/gui/src/app/state/mod.rs
index 1c3e648f..a6fc78dc 100644
--- a/gui/src/app/state/mod.rs
+++ b/gui/src/app/state/mod.rs
@@ -39,7 +39,7 @@ pub trait State {
 }
 
 pub struct Home {
-    wallet: Wallet,
+    wallet: Arc<Wallet>,
     balance: Amount,
     recovery_warning: Option<(Amount, usize)>,
     recovery_alert: Option<(Amount, usize)>,
@@ -50,7 +50,7 @@ pub struct Home {
 }
 
 impl Home {
-    pub fn new(wallet: Wallet, coins: &[Coin]) -> Self {
+    pub fn new(wallet: Arc<Wallet>, coins: &[Coin]) -> Self {
         Self {
             wallet,
             balance: Amount::from_sat(
diff --git a/gui/src/app/state/recovery.rs b/gui/src/app/state/recovery.rs
index cdfadef9..ecf35c26 100644
--- a/gui/src/app/state/recovery.rs
+++ b/gui/src/app/state/recovery.rs
@@ -6,48 +6,37 @@ use iced::{Command, Element};
 use crate::{
     app::{
         cache::Cache,
-        config::Config,
         error::Error,
         menu::Menu,
         message::Message,
+        state::spend::detail,
         state::{redirect, State},
         view,
         wallet::Wallet,
     },
     daemon::{
-        model::{remaining_sequence, Coin},
+        model::{remaining_sequence, Coin, SpendTx},
         Daemon,
     },
-    hw::{list_hardware_wallets, HardwareWallet},
     ui::component::form,
 };
 
-use liana::miniscript::bitcoin::{util::psbt::Psbt, Address, Amount, Network};
+use liana::miniscript::bitcoin::{Address, Amount, Network};
 
 pub struct RecoveryPanel {
-    wallet: Wallet,
-    config: Config,
+    wallet: Arc<Wallet>,
     locked_coins: (usize, Amount),
     recoverable_coins: (usize, Amount),
     warning: Option<Error>,
     feerate: form::Value<String>,
     recipient: form::Value<String>,
-    generated: Option<Psbt>,
-    hws: Vec<HardwareWallet>,
-    selected_hw: Option<usize>,
-    signed: bool,
+    generated: Option<detail::SpendTxState>,
     /// timelock value to pass for the heir to consume a coin.
     timelock: u32,
 }
 
 impl RecoveryPanel {
-    pub fn new(
-        wallet: Wallet,
-        config: Config,
-        coins: &[Coin],
-        timelock: u32,
-        blockheight: u32,
-    ) -> Self {
+    pub fn new(wallet: Arc<Wallet>, coins: &[Coin], timelock: u32, blockheight: u32) -> Self {
         let mut locked_coins = (0, Amount::from_sat(0));
         let mut recoverable_coins = (0, Amount::from_sat(0));
         for coin in coins {
@@ -64,7 +53,6 @@ impl RecoveryPanel {
         }
         Self {
             wallet,
-            config,
             locked_coins,
             recoverable_coins,
             warning: None,
@@ -72,30 +60,27 @@ impl RecoveryPanel {
             recipient: form::Value::default(),
             generated: None,
             timelock,
-            hws: Vec::new(),
-            selected_hw: None,
-            signed: false,
         }
     }
 }
 
 impl State for RecoveryPanel {
-    fn view<'a>(&'a self, _cache: &'a Cache) -> Element<'a, view::Message> {
-        view::modal(
-            false,
-            self.warning.as_ref(),
-            view::recovery::recovery(
-                &self.locked_coins,
-                &self.recoverable_coins,
-                &self.feerate,
-                &self.recipient,
-                self.generated.as_ref(),
-                &self.hws,
-                self.selected_hw,
-                self.signed,
-            ),
-            None::<Element<view::Message>>,
-        )
+    fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> {
+        if let Some(generated) = &self.generated {
+            generated.view(cache)
+        } else {
+            view::modal(
+                false,
+                self.warning.as_ref(),
+                view::recovery::recovery(
+                    &self.locked_coins,
+                    &self.recoverable_coins,
+                    &self.feerate,
+                    &self.recipient,
+                ),
+                None::<Element<view::Message>>,
+            )
+        }
     }
 
     fn update(
@@ -127,27 +112,13 @@ impl State for RecoveryPanel {
                     }
                 }
             },
-            // We add the new hws without dropping the reference of the previous ones.
-            Message::ConnectedHardwareWallets(hws) => {
-                for h in hws {
-                    if !self.hws.iter().any(|hw| hw.fingerprint == h.fingerprint) {
-                        self.hws.push(h);
-                    }
+            Message::Recovery(res) => match res {
+                Ok(tx) => {
+                    self.generated = Some(detail::SpendTxState::new(self.wallet.clone(), tx, false))
                 }
-            }
-            Message::Psbt(res) => match res {
-                Ok(psbt) => self.generated = Some(psbt),
                 Err(e) => self.warning = Some(e),
             },
-            Message::Updated(res) => match res {
-                Err(e) => self.warning = Some(e),
-                Ok(()) => {
-                    self.warning = None;
-                    self.signed = true;
-                }
-            },
             Message::View(msg) => match msg {
-                view::Message::Reload => return self.load(daemon),
                 view::Message::Close => return redirect(Menu::Settings),
                 view::Message::Previous => self.generated = None,
                 view::Message::CreateSpend(view::CreateSpendMessage::RecipientEdited(
@@ -175,70 +146,56 @@ impl State for RecoveryPanel {
                     let address = Address::from_str(&self.recipient.value).expect("Checked before");
                     let feerate_vb = self.feerate.value.parse::<u64>().expect("Checked before");
                     self.warning = None;
+                    let desc = self.wallet.main_descriptor.clone();
                     return Command::perform(
                         async move {
-                            daemon
-                                .create_recovery(address, feerate_vb)
-                                .map_err(|e| e.into())
+                            let psbt = daemon.create_recovery(address, feerate_vb)?;
+                            let coins = daemon.list_coins().map(|res| res.coins)?;
+                            let coins = coins
+                                .iter()
+                                .filter(|coin| {
+                                    psbt.unsigned_tx
+                                        .input
+                                        .iter()
+                                        .any(|input| input.previous_output == coin.outpoint)
+                                })
+                                .copied()
+                                .collect();
+                            let sigs = desc.partial_spend_info(&psbt).unwrap();
+                            Ok(SpendTx::new(psbt, coins, sigs))
                         },
-                        Message::Psbt,
+                        Message::Recovery,
                     );
                 }
-                view::Message::Spend(view::SpendTxMessage::SelectHardwareWallet(i)) => {
-                    if let Some(hw) = self.hws.get(i) {
-                        let device = hw.device.clone();
-                        self.selected_hw = Some(i);
-                        let psbt = self.generated.clone().unwrap();
-                        return Command::perform(
-                            send_funds(daemon, device, psbt),
-                            Message::Updated,
-                        );
+                _ => {
+                    if let Some(generated) = &mut self.generated {
+                        return generated.update(daemon, cache, Message::View(msg));
                     }
                 }
-                _ => {}
             },
-            _ => {}
+            _ => {
+                if let Some(generated) = &mut self.generated {
+                    return generated.update(daemon, cache, message);
+                }
+            }
         };
         Command::none()
     }
 
     fn load(&self, daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
-        let config = self.config.clone();
-        let desc = self.wallet.main_descriptor.to_string();
         let daemon = daemon.clone();
-        Command::batch(vec![
-            Command::perform(
-                async move {
-                    daemon
-                        .list_coins()
-                        .map(|res| res.coins)
-                        .map_err(|e| e.into())
-                },
-                Message::Coins,
-            ),
-            Command::perform(
-                list_hws(config, self.wallet.name.clone(), desc),
-                Message::ConnectedHardwareWallets,
-            ),
-        ])
+        Command::perform(
+            async move {
+                daemon
+                    .list_coins()
+                    .map(|res| res.coins)
+                    .map_err(|e| e.into())
+            },
+            Message::Coins,
+        )
     }
 }
 
-async fn list_hws(config: Config, wallet_name: String, descriptor: String) -> Vec<HardwareWallet> {
-    list_hardware_wallets(&config.hardware_wallets, Some((&wallet_name, &descriptor))).await
-}
-
-async fn send_funds(
-    daemon: Arc<dyn Daemon + Sync + Send>,
-    hw: std::sync::Arc<dyn async_hwi::HWI + Send + Sync>,
-    mut psbt: Psbt,
-) -> Result<(), Error> {
-    hw.sign_tx(&mut psbt).await.map_err(Error::from)?;
-    daemon.update_spend_tx(&psbt)?;
-    daemon.broadcast_spend_tx(&psbt.unsigned_tx.txid())?;
-    Ok(())
-}
-
 impl From<RecoveryPanel> for Box<dyn State> {
     fn from(s: RecoveryPanel) -> Box<dyn State> {
         Box::new(s)
diff --git a/gui/src/app/state/spend/detail.rs b/gui/src/app/state/spend/detail.rs
index c6a34d64..81479859 100644
--- a/gui/src/app/state/spend/detail.rs
+++ b/gui/src/app/state/spend/detail.rs
@@ -1,15 +1,17 @@
 use std::sync::Arc;
 
 use iced::{Command, Element};
-use liana::miniscript::bitcoin::{
-    consensus,
-    util::{bip32::Fingerprint, psbt::Psbt},
+use liana::{
+    descriptors::LianaDescInfo,
+    miniscript::bitcoin::{
+        consensus,
+        util::{bip32::Fingerprint, psbt::Psbt},
+    },
 };
 
 use crate::{
     app::{
-        cache::Cache, config::Config, error::Error, message::Message, view, view::spend::detail,
-        wallet::Wallet,
+        cache::Cache, error::Error, message::Message, view, view::spend::detail, wallet::Wallet,
     },
     daemon::{
         model::{SpendStatus, SpendTx},
@@ -39,19 +41,19 @@ trait Action {
 }
 
 pub struct SpendTxState {
-    wallet: Wallet,
-    config: Config,
+    wallet: Arc<Wallet>,
+    desc_info: LianaDescInfo,
     tx: SpendTx,
     saved: bool,
     action: Option<Box<dyn Action>>,
 }
 
 impl SpendTxState {
-    pub fn new(wallet: Wallet, config: Config, tx: SpendTx, saved: bool) -> Self {
+    pub fn new(wallet: Arc<Wallet>, tx: SpendTx, saved: bool) -> Self {
         Self {
+            desc_info: wallet.main_descriptor.info(),
             wallet,
             action: None,
-            config,
             tx,
             saved,
         }
@@ -80,7 +82,7 @@ impl SpendTxState {
                     self.action = Some(Box::new(DeleteAction::default()));
                 }
                 view::SpendTxMessage::Sign => {
-                    let action = SignAction::new(self.config.clone());
+                    let action = SignAction::new();
                     let cmd = action.load(&self.wallet, daemon);
                     self.action = Some(Box::new(action));
                     return cmd;
@@ -119,7 +121,13 @@ impl SpendTxState {
     }
 
     pub fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> {
-        let content = detail::spend_view(&self.tx, self.saved, cache.network);
+        let content = detail::spend_view(
+            &self.tx,
+            self.saved,
+            &self.desc_info,
+            &self.wallet.keys_aliases,
+            cache.network,
+        );
         if let Some(action) = &self.action {
             modal::Modal::new(content, action.view())
                 .on_blur(Some(view::Message::Spend(view::SpendTxMessage::Cancel)))
@@ -252,7 +260,6 @@ impl Action for DeleteAction {
 }
 
 pub struct SignAction {
-    config: Config,
     chosen_hw: Option<usize>,
     processing: bool,
     hws: Vec<HardwareWallet>,
@@ -261,9 +268,8 @@ pub struct SignAction {
 }
 
 impl SignAction {
-    pub fn new(config: Config) -> Self {
+    pub fn new() -> Self {
         Self {
-            config,
             chosen_hw: None,
             processing: false,
             hws: Vec::new(),
@@ -279,13 +285,8 @@ impl Action for SignAction {
     }
 
     fn load(&self, wallet: &Wallet, _daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
-        let config = self.config.clone();
-        let desc = wallet.main_descriptor.to_string();
-        let name = wallet.name.clone();
-        Command::perform(
-            list_hws(config, name, desc),
-            Message::ConnectedHardwareWallets,
-        )
+        let wallet = wallet.clone();
+        Command::perform(list_hws(wallet), Message::ConnectedHardwareWallets)
     }
     fn update(
         &mut self,
@@ -321,7 +322,10 @@ impl Action for SignAction {
                 }
             },
             Message::Updated(res) => match res {
-                Ok(()) => self.processing = false,
+                Ok(()) => {
+                    self.processing = false;
+                    tx.sigs = wallet.main_descriptor.partial_spend_info(&tx.psbt).unwrap();
+                }
                 Err(e) => self.error = Some(e),
             },
             // We add the new hws without dropping the reference of the previous ones.
@@ -350,8 +354,12 @@ impl Action for SignAction {
     }
 }
 
-async fn list_hws(config: Config, wallet_name: String, descriptor: String) -> Vec<HardwareWallet> {
-    list_hardware_wallets(&config.hardware_wallets, Some((&wallet_name, &descriptor))).await
+async fn list_hws(wallet: Wallet) -> Vec<HardwareWallet> {
+    list_hardware_wallets(
+        &wallet.hardware_wallets,
+        Some((&wallet.name, &wallet.main_descriptor.to_string())),
+    )
+    .await
 }
 
 async fn sign_psbt(
@@ -399,7 +407,7 @@ impl Action for UpdateAction {
 
     fn update(
         &mut self,
-        _wallet: &Wallet,
+        wallet: &Wallet,
         daemon: Arc<dyn Daemon + Sync + Send>,
         message: Message,
         tx: &mut SpendTx,
@@ -436,6 +444,7 @@ impl Action for UpdateAction {
                                     .extend(updated_input.partial_sigs.clone().into_iter());
                             }
                         }
+                        tx.sigs = wallet.main_descriptor.partial_spend_info(&tx.psbt).unwrap();
                     }
                     Err(e) => self.error = e.into(),
                 }
diff --git a/gui/src/app/state/spend/mod.rs b/gui/src/app/state/spend/mod.rs
index ddc4368e..230167f5 100644
--- a/gui/src/app/state/spend/mod.rs
+++ b/gui/src/app/state/spend/mod.rs
@@ -1,4 +1,4 @@
-mod detail;
+pub mod detail;
 mod step;
 use std::sync::Arc;
 
@@ -8,10 +8,7 @@ use liana::miniscript::bitcoin::{consensus, util::psbt::Psbt};
 
 use super::{redirect, State};
 use crate::{
-    app::{
-        cache::Cache, config::Config, error::Error, menu::Menu, message::Message, view,
-        wallet::Wallet,
-    },
+    app::{cache::Cache, error::Error, menu::Menu, message::Message, view, wallet::Wallet},
     daemon::{
         model::{Coin, SpendTx},
         Daemon,
@@ -20,8 +17,7 @@ use crate::{
 };
 
 pub struct SpendPanel {
-    wallet: Wallet,
-    config: Config,
+    wallet: Arc<Wallet>,
     selected_tx: Option<detail::SpendTxState>,
     spend_txs: Vec<SpendTx>,
     warning: Option<Error>,
@@ -29,10 +25,9 @@ pub struct SpendPanel {
 }
 
 impl SpendPanel {
-    pub fn new(wallet: Wallet, config: Config, spend_txs: &[SpendTx]) -> Self {
+    pub fn new(wallet: Arc<Wallet>, spend_txs: &[SpendTx]) -> Self {
         Self {
             wallet,
-            config,
             spend_txs: spend_txs.to_vec(),
             warning: None,
             selected_tx: None,
@@ -97,12 +92,7 @@ impl State for SpendPanel {
             }
             Message::View(view::Message::Select(i)) => {
                 if let Some(tx) = self.spend_txs.get(i) {
-                    let tx = detail::SpendTxState::new(
-                        self.wallet.clone(),
-                        self.config.clone(),
-                        tx.clone(),
-                        true,
-                    );
+                    let tx = detail::SpendTxState::new(self.wallet.clone(), tx.clone(), true);
                     let cmd = tx.load(daemon);
                     self.selected_tx = Some(tx);
                     return cmd;
@@ -143,7 +133,7 @@ pub struct CreateSpendPanel {
 }
 
 impl CreateSpendPanel {
-    pub fn new(wallet: Wallet, config: Config, coins: &[Coin], blockheight: u32) -> Self {
+    pub fn new(wallet: Arc<Wallet>, coins: &[Coin], blockheight: u32) -> Self {
         let descriptor = wallet.main_descriptor.clone();
         let timelock = descriptor.timelock_value();
         Self {
@@ -157,7 +147,7 @@ impl CreateSpendPanel {
                     timelock,
                     blockheight,
                 )),
-                Box::new(step::SaveSpend::new(wallet, config)),
+                Box::new(step::SaveSpend::new(wallet)),
             ],
         }
     }
diff --git a/gui/src/app/state/spend/step.rs b/gui/src/app/state/spend/step.rs
index eae21155..0280b990 100644
--- a/gui/src/app/state/spend/step.rs
+++ b/gui/src/app/state/spend/step.rs
@@ -12,8 +12,7 @@ use liana::{
 
 use crate::{
     app::{
-        cache::Cache, config::Config, error::Error, message::Message, state::spend::detail, view,
-        wallet::Wallet,
+        cache::Cache, error::Error, message::Message, state::spend::detail, view, wallet::Wallet,
     },
     daemon::{
         model::{remaining_sequence, Coin, SpendTx},
@@ -430,16 +429,14 @@ impl Step for ChooseCoins {
 }
 
 pub struct SaveSpend {
-    wallet: Wallet,
-    config: Config,
+    wallet: Arc<Wallet>,
     spend: Option<detail::SpendTxState>,
 }
 
 impl SaveSpend {
-    pub fn new(wallet: Wallet, config: Config) -> Self {
+    pub fn new(wallet: Arc<Wallet>) -> Self {
         Self {
             wallet,
-            config,
             spend: None,
         }
     }
@@ -447,10 +444,15 @@ impl SaveSpend {
 
 impl Step for SaveSpend {
     fn load(&mut self, draft: &TransactionDraft) {
+        let psbt = draft.generated.clone().unwrap();
+        let sigs = self
+            .wallet
+            .main_descriptor
+            .partial_spend_info(&psbt)
+            .unwrap();
         self.spend = Some(detail::SpendTxState::new(
             self.wallet.clone(),
-            self.config.clone(),
-            SpendTx::new(draft.generated.clone().unwrap(), draft.inputs.clone()),
+            SpendTx::new(psbt, draft.inputs.clone(), sigs),
             false,
         ));
     }
diff --git a/gui/src/app/view/recovery.rs b/gui/src/app/view/recovery.rs
index 15b14c73..0fd90b27 100644
--- a/gui/src/app/view/recovery.rs
+++ b/gui/src/app/view/recovery.rs
@@ -1,18 +1,14 @@
 use iced::{
-    widget::{Button, Column, Container, Row, Space},
+    widget::{Column, Container, Row, Space},
     Alignment, Element, Length,
 };
 
-use liana::miniscript::bitcoin::{util::psbt::Psbt, Amount};
+use liana::miniscript::bitcoin::Amount;
 
 use crate::{
-    app::view::{
-        hw::hw_list_view,
-        message::{CreateSpendMessage, Message},
-    },
-    hw::HardwareWallet,
+    app::view::message::{CreateSpendMessage, Message},
     ui::{
-        component::{button, card, form, text::*},
+        component::{button, form, text::*},
         icon,
         util::Collection,
     },
@@ -24,10 +20,6 @@ pub fn recovery<'a>(
     recoverable_coins: &(usize, Amount),
     feerate: &form::Value<String>,
     address: &'a form::Value<String>,
-    generated: Option<&Psbt>,
-    hws: &[HardwareWallet],
-    chosen_hw: Option<usize>,
-    done: bool,
 ) -> Element<'a, Message> {
     Column::new()
         .push(Space::with_height(Length::Units(100)))
@@ -59,173 +51,7 @@ pub fn recovery<'a>(
             None
         })
         .push(Space::with_height(Length::Units(20)))
-        .push(if let Some(psbt) = generated {
-            if done {
-                Column::new()
-                    .spacing(20)
-                    .align_items(Alignment::Center)
-                    .push(text("Funds were sweeped"))
-                    .push(card::simple(
-                        Column::new()
-                            .push(
-                                Row::new()
-                                    .spacing(5)
-                                    .align_items(Alignment::Center)
-                                    .push(
-                                        text(format!(
-                                            "{}",
-                                            Amount::from_sat(psbt.unsigned_tx.output[0].value)
-                                        ))
-                                        .small()
-                                        .bold(),
-                                    )
-                                    .push(text(" to ").small())
-                                    .push(text(&address.value).small().bold()),
-                            )
-                            .push(
-                                Row::new()
-                                    .spacing(5)
-                                    .align_items(Alignment::Center)
-                                    .push(
-                                        text(format!("Txid: {}", psbt.unsigned_tx.txid())).small(),
-                                    )
-                                    .push(
-                                        Button::new(icon::clipboard_icon().small())
-                                            .on_press(Message::Clipboard(
-                                                psbt.unsigned_tx.txid().to_string(),
-                                            ))
-                                            .style(button::Style::Border.into()),
-                                    ),
-                            )
-                            .push_maybe(
-                                if recoverable_coins.1.to_sat() > psbt.unsigned_tx.output[0].value {
-                                    Some(
-                                        Row::new().push(
-                                            text(format!(
-                                                "Fees: {}",
-                                                recoverable_coins.1
-                                                    - Amount::from_sat(
-                                                        psbt.unsigned_tx.output[0].value
-                                                    )
-                                            ))
-                                            .small(),
-                                        ),
-                                    )
-                                } else {
-                                    None
-                                },
-                            ),
-                    ))
-            } else {
-                Column::new()
-                    .spacing(20)
-                    .align_items(Alignment::Center)
-                    .push_maybe(if chosen_hw.is_none() {
-                        Some(button::border(None, "< Previous").on_press(Message::Previous))
-                    } else {
-                        None
-                    })
-                    .push(text("Sign the transaction to sweep the funds").bold())
-                    .push(card::simple(
-                        Column::new()
-                            .push(
-                                Row::new()
-                                    .spacing(5)
-                                    .align_items(Alignment::Center)
-                                    .push(
-                                        text(format!(
-                                            "{}",
-                                            Amount::from_sat(psbt.unsigned_tx.output[0].value)
-                                        ))
-                                        .small()
-                                        .bold(),
-                                    )
-                                    .push(text(" to ").small())
-                                    .push(text(&address.value).small().bold()),
-                            )
-                            .push(
-                                Row::new()
-                                    .spacing(5)
-                                    .align_items(Alignment::Center)
-                                    .push(
-                                        text(format!("Txid: {}", psbt.unsigned_tx.txid())).small(),
-                                    )
-                                    .push(
-                                        Button::new(icon::clipboard_icon().small())
-                                            .on_press(Message::Clipboard(
-                                                psbt.unsigned_tx.txid().to_string(),
-                                            ))
-                                            .style(button::Style::Border.into()),
-                                    ),
-                            )
-                            .push_maybe(
-                                if recoverable_coins.1.to_sat() > psbt.unsigned_tx.output[0].value {
-                                    Some(
-                                        Row::new().push(
-                                            text(format!(
-                                                "Fees: {}",
-                                                recoverable_coins.1
-                                                    - Amount::from_sat(
-                                                        psbt.unsigned_tx.output[0].value
-                                                    )
-                                            ))
-                                            .small(),
-                                        ),
-                                    )
-                                } else {
-                                    None
-                                },
-                            ),
-                    ))
-                    .push(if !hws.is_empty() {
-                        Column::new()
-                            .push(
-                                Row::new()
-                                    .align_items(Alignment::Center)
-                                    .push(
-                                        text("Select hardware wallet to sign with:")
-                                            .bold()
-                                            .width(Length::Fill),
-                                    )
-                                    .push_maybe(if chosen_hw.is_none() {
-                                        Some(
-                                            button::border(None, "Refresh")
-                                                .on_press(Message::Reload),
-                                        )
-                                    } else {
-                                        None
-                                    }),
-                            )
-                            .spacing(10)
-                            .push(hws.iter().enumerate().fold(
-                                Column::new().spacing(10),
-                                |col, (i, hw)| {
-                                    col.push(hw_list_view(
-                                        i,
-                                        hw,
-                                        Some(i) == chosen_hw,
-                                        chosen_hw.is_some(),
-                                        false,
-                                    ))
-                                },
-                            ))
-                            .max_width(500)
-                    } else {
-                        Column::new()
-                            .push(
-                                Column::new()
-                                    .spacing(20)
-                                    .width(Length::Fill)
-                                    .push("Please connect a hardware wallet")
-                                    .push(
-                                        button::primary(None, "Refresh").on_press(Message::Reload),
-                                    )
-                                    .align_items(Alignment::Center),
-                            )
-                            .width(Length::Fill)
-                    })
-            }
-        } else {
+        .push(
             Column::new()
                 .push(text("Enter destination address and feerate:").bold())
                 .push(
@@ -267,8 +93,8 @@ pub fn recovery<'a>(
                     },
                 )
                 .spacing(20)
-                .align_items(Alignment::Center)
-        })
+                .align_items(Alignment::Center),
+        )
         .align_items(Alignment::Center)
         .spacing(20)
         .into()
diff --git a/gui/src/app/view/spend/detail.rs b/gui/src/app/view/spend/detail.rs
index 643392d2..a1a8267c 100644
--- a/gui/src/app/view/spend/detail.rs
+++ b/gui/src/app/view/spend/detail.rs
@@ -1,9 +1,14 @@
+use std::collections::HashMap;
+
 use iced::{
-    widget::{Button, Column, Container, Row, Scrollable, Space},
+    widget::{scrollable, tooltip, Button, Column, Container, Row, Scrollable, Space},
     Alignment, Element, Length,
 };
 
-use liana::miniscript::bitcoin::{util::bip32::Fingerprint, Address, Amount, Network, Transaction};
+use liana::{
+    descriptors::{LianaDescInfo, PathInfo, PathSpendInfo},
+    miniscript::bitcoin::{util::bip32::Fingerprint, Address, Amount, Network, Transaction},
+};
 
 use crate::{
     app::{
@@ -25,7 +30,13 @@ use crate::{
     },
 };
 
-pub fn spend_view(tx: &SpendTx, saved: bool, network: Network) -> Element<Message> {
+pub fn spend_view<'a>(
+    tx: &'a SpendTx,
+    saved: bool,
+    desc_info: &'a LianaDescInfo,
+    key_aliases: &'a HashMap<Fingerprint, String>,
+    network: Network,
+) -> Element<'a, Message> {
     spend_modal(
         saved,
         None,
@@ -33,7 +44,7 @@ pub fn spend_view(tx: &SpendTx, saved: bool, network: Network) -> Element<Messag
             .align_items(Alignment::Center)
             .spacing(20)
             .push(spend_header(tx))
-            .push(spend_overview_view(tx))
+            .push(spend_overview_view(tx, desc_info, key_aliases))
             .push(inputs_and_outputs_view(
                 &tx.coins,
                 &tx.psbt.unsigned_tx,
@@ -187,7 +198,11 @@ fn spend_header<'a>(tx: &SpendTx) -> Element<'a, Message> {
         .push(
             Row::new()
                 .push(badge::Badge::new(icon::send_icon()).style(badge::Style::Standard))
-                .push(text("Spend").bold())
+                .push(if tx.sigs.recovery_path().is_some() {
+                    text("Recovery").bold()
+                } else {
+                    text("Spend").bold()
+                })
                 .spacing(5)
                 .align_items(Alignment::Center),
         )
@@ -217,67 +232,17 @@ fn spend_header<'a>(tx: &SpendTx) -> Element<'a, Message> {
         .into()
 }
 
-fn spend_overview_view<'a>(tx: &SpendTx) -> Element<'a, Message> {
-    card::simple(
+fn spend_overview_view<'a>(
+    tx: &'a SpendTx,
+    desc_info: &'a LianaDescInfo,
+    key_aliases: &'a HashMap<Fingerprint, String>,
+) -> Element<'a, Message> {
+    Container::new(
         Column::new()
-            .push(Container::new(
-                Row::new()
-                    .push(
-                        Container::new(
-                            Row::new()
-                                .push(Container::new(
-                                    icon::key_icon().size(30).width(Length::Fill),
-                                ))
-                                .push(
-                                    Column::new()
-                                        .push(text("Number of signatures:").bold())
-                                        .push(text(format!(
-                                            "{}",
-                                            tx.psbt.inputs[0].partial_sigs.len(),
-                                        )))
-                                        .width(Length::Fill),
-                                )
-                                .push_maybe(if tx.status == SpendStatus::Pending {
-                                    if !tx.is_signed() {
-                                        Some(
-                                            button::primary(None, "Sign")
-                                                .on_press(Message::Spend(SpendTxMessage::Sign)),
-                                        )
-                                    } else {
-                                        Some(
-                                            button::primary(None, "Broadcast").on_press(
-                                                Message::Spend(SpendTxMessage::Broadcast),
-                                            ),
-                                        )
-                                    }
-                                } else {
-                                    None
-                                })
-                                .align_items(Alignment::Center)
-                                .spacing(20),
-                        )
-                        .width(Length::FillPortion(1)),
-                    )
-                    .align_items(Alignment::Center)
-                    .spacing(20),
-            ))
-            .push(separation().width(Length::Fill))
             .push(
                 Column::new()
+                    .padding(15)
                     .spacing(10)
-                    .push(
-                        Row::new()
-                            .push(text("Tx ID:").bold().width(Length::Fill))
-                            .push(text(tx.psbt.unsigned_tx.txid().to_string()).small())
-                            .push(
-                                Button::new(icon::clipboard_icon())
-                                    .on_press(Message::Clipboard(
-                                        tx.psbt.unsigned_tx.txid().to_string(),
-                                    ))
-                                    .style(button::Style::TransparentBorder.into()),
-                            )
-                            .align_items(Alignment::Center),
-                    )
                     .push(
                         Row::new()
                             .align_items(Alignment::Center)
@@ -295,10 +260,209 @@ fn spend_overview_view<'a>(tx: &SpendTx) -> Element<'a, Message> {
                                     ),
                             )
                             .align_items(Alignment::Center),
+                    )
+                    .push(
+                        Row::new()
+                            .push(text("Tx ID:").bold().width(Length::Fill))
+                            .push(text(tx.psbt.unsigned_tx.txid().to_string()).small())
+                            .push(
+                                Button::new(icon::clipboard_icon())
+                                    .on_press(Message::Clipboard(
+                                        tx.psbt.unsigned_tx.txid().to_string(),
+                                    ))
+                                    .style(button::Style::TransparentBorder.into()),
+                            )
+                            .align_items(Alignment::Center),
                     ),
             )
-            .spacing(20),
+            .push(signatures(tx, desc_info, key_aliases)),
     )
+    .style(card::SimpleCardStyle)
+    .into()
+}
+
+pub fn signatures<'a>(
+    tx: &'a SpendTx,
+    desc_info: &'a LianaDescInfo,
+    keys_aliases: &'a HashMap<Fingerprint, String>,
+) -> Element<'a, Message> {
+    Column::new()
+        .push(Collapse::new(
+            move || {
+                Button::new(
+                    Row::new()
+                        .align_items(Alignment::Center)
+                        .push(if tx.is_ready() {
+                            Row::new()
+                                .spacing(5)
+                                .align_items(Alignment::Center)
+                                .push(icon::circle_check_icon().style(color::SUCCESS))
+                                .push(text("Ready").bold().style(color::SUCCESS))
+                                .width(Length::Fill)
+                        } else {
+                            Row::new()
+                                .spacing(5)
+                                .align_items(Alignment::Center)
+                                .push(icon::circle_cross_icon())
+                                .push(text("Not ready").bold())
+                                .width(Length::Fill)
+                        })
+                        .push(icon::collapse_icon()),
+                )
+                .padding(15)
+                .width(Length::Fill)
+                .style(button::Style::TransparentBorder.into())
+            },
+            move || {
+                Button::new(
+                    Row::new()
+                        .align_items(Alignment::Center)
+                        .push(if tx.is_ready() {
+                            Row::new()
+                                .spacing(5)
+                                .align_items(Alignment::Center)
+                                .push(icon::circle_check_icon().style(color::SUCCESS))
+                                .push(text("Ready").bold().style(color::SUCCESS))
+                                .width(Length::Fill)
+                        } else {
+                            Row::new()
+                                .spacing(5)
+                                .align_items(Alignment::Center)
+                                .push(icon::circle_cross_icon())
+                                .push(text("Not ready").bold())
+                                .width(Length::Fill)
+                        })
+                        .push(icon::collapsed_icon()),
+                )
+                .padding(15)
+                .width(Length::Fill)
+                .style(button::Style::TransparentBorder.into())
+            },
+            move || {
+                Into::<Element<'a, Message>>::into(
+                    Column::new().push(separation().width(Length::Fill)).push(
+                        Column::new()
+                            .padding(15)
+                            .spacing(10)
+                            .push(path_view(
+                                desc_info.primary_path(),
+                                tx.sigs.primary_path(),
+                                keys_aliases,
+                            ))
+                            .push_maybe(tx.sigs.recovery_path().as_ref().map(|path| {
+                                let (_, keys) = desc_info.recovery_path();
+                                path_view(keys, path, keys_aliases)
+                            })),
+                    ),
+                )
+            },
+        ))
+        .push_maybe(if tx.status == SpendStatus::Pending {
+            Some(
+                Column::new().push(separation().width(Length::Fill)).push(
+                    Container::new(
+                        Row::new()
+                            .push(Space::with_width(Length::Fill))
+                            .push_maybe(if !tx.is_ready() {
+                                Some(
+                                    button::primary(None, "Sign")
+                                        .on_press(Message::Spend(SpendTxMessage::Sign))
+                                        .width(Length::Units(150)),
+                                )
+                            } else {
+                                Some(
+                                    button::primary(None, "Broadcast")
+                                        .on_press(Message::Spend(SpendTxMessage::Broadcast))
+                                        .width(Length::Units(150)),
+                                )
+                            })
+                            .align_items(Alignment::Center)
+                            .spacing(20),
+                    )
+                    .padding(15),
+                ),
+            )
+        } else {
+            None
+        })
+        .into()
+}
+
+pub fn path_view<'a>(
+    path: &'a PathInfo,
+    sigs: &'a PathSpendInfo,
+    key_aliases: &'a HashMap<Fingerprint, String>,
+) -> Element<'a, Message> {
+    let mut keys: Vec<Fingerprint> = path.thresh_fingerprints().1.into_iter().collect();
+    keys.sort();
+    Scrollable::new(
+        Row::new()
+            .spacing(5)
+            .align_items(Alignment::Center)
+            .push(if sigs.signed_pubkeys.len() >= sigs.threshold {
+                icon::circle_check_icon().style(color::SUCCESS)
+            } else {
+                icon::circle_cross_icon()
+            })
+            .push(
+                Container::new(text(format!("  {}  ", sigs.threshold))).style(
+                    if sigs.signed_pubkeys.len() >= sigs.threshold {
+                        badge::PillStyle::Success
+                    } else {
+                        badge::PillStyle::Simple
+                    },
+                ),
+            )
+            .push(text(format!(
+                "signature{} out of",
+                if sigs.threshold > 1 { "s" } else { "" }
+            )))
+            .push(
+                sigs.signed_pubkeys
+                    .keys()
+                    .fold(Row::new().spacing(5), |row, value| {
+                        row.push(if let Some(alias) = key_aliases.get(value) {
+                            Container::new(
+                                tooltip::Tooltip::new(
+                                    Container::new(text(alias))
+                                        .padding(3)
+                                        .style(badge::PillStyle::Success),
+                                    value.to_string(),
+                                    tooltip::Position::Bottom,
+                                )
+                                .style(card::SimpleCardStyle),
+                            )
+                        } else {
+                            Container::new(text(value.to_string()))
+                                .padding(3)
+                                .style(badge::PillStyle::Success)
+                        })
+                    }),
+            )
+            .push(keys.iter().fold(Row::new().spacing(5), |row, &value| {
+                row.push_maybe(if !sigs.signed_pubkeys.contains_key(&value) {
+                    Some(if let Some(alias) = key_aliases.get(&value) {
+                        Container::new(
+                            tooltip::Tooltip::new(
+                                Container::new(text(alias))
+                                    .padding(3)
+                                    .style(badge::PillStyle::Simple),
+                                value.to_string(),
+                                tooltip::Position::Bottom,
+                            )
+                            .style(card::SimpleCardStyle),
+                        )
+                    } else {
+                        Container::new(text(value.to_string()))
+                            .padding(3)
+                            .style(badge::PillStyle::Simple)
+                    })
+                } else {
+                    None
+                })
+            })),
+    )
+    .horizontal_scroll(scrollable::Properties::new().width(2).scroller_width(2))
     .into()
 }
 
@@ -513,26 +677,26 @@ pub fn sign_action<'a>(
     chosen_hw: Option<usize>,
     signed: &[Fingerprint],
 ) -> Element<'a, Message> {
-    card::simple(
-        Column::new()
-            .push_maybe(warning.map(|w| warn(Some(w))))
-            .push(if !hws.is_empty() {
-                Column::new()
-                    .push(
-                        Row::new()
-                            .push(
-                                text("Select hardware wallet to sign with:")
-                                    .bold()
-                                    .width(Length::Fill),
-                            )
-                            .push(button::border(None, "Refresh").on_press(Message::Reload))
-                            .align_items(Alignment::Center),
-                    )
-                    .spacing(10)
-                    .push(
-                        hws.iter()
-                            .enumerate()
-                            .fold(Column::new().spacing(10), |col, (i, hw)| {
+    Column::new()
+        .push_maybe(warning.map(|w| warn(Some(w))))
+        .push(card::simple(
+            Column::new()
+                .push(if !hws.is_empty() {
+                    Column::new()
+                        .push(
+                            Row::new()
+                                .push(
+                                    text("Select hardware wallet to sign with:")
+                                        .bold()
+                                        .width(Length::Fill),
+                                )
+                                .push(button::border(None, "Refresh").on_press(Message::Reload))
+                                .align_items(Alignment::Center),
+                        )
+                        .spacing(10)
+                        .push(hws.iter().enumerate().fold(
+                            Column::new().spacing(10),
+                            |col, (i, hw)| {
                                 col.push(hw_list_view(
                                     i,
                                     hw,
@@ -540,27 +704,27 @@ pub fn sign_action<'a>(
                                     processing,
                                     signed.contains(&hw.fingerprint),
                                 ))
-                            }),
-                    )
-                    .width(Length::Fill)
-            } else {
-                Column::new()
-                    .push(
-                        Column::new()
-                            .spacing(15)
-                            .width(Length::Fill)
-                            .push("Please connect a hardware wallet")
-                            .push(button::border(None, "Refresh").on_press(Message::Reload))
-                            .align_items(Alignment::Center),
-                    )
-                    .width(Length::Fill)
-            })
-            .spacing(20)
-            .width(Length::Fill)
-            .align_items(Alignment::Center),
-    )
-    .width(Length::Units(500))
-    .into()
+                            },
+                        ))
+                        .width(Length::Fill)
+                } else {
+                    Column::new()
+                        .push(
+                            Column::new()
+                                .spacing(15)
+                                .width(Length::Fill)
+                                .push("Please connect a hardware wallet")
+                                .push(button::border(None, "Refresh").on_press(Message::Reload))
+                                .align_items(Alignment::Center),
+                        )
+                        .width(Length::Fill)
+                })
+                .spacing(20)
+                .width(Length::Fill)
+                .align_items(Alignment::Center),
+        ))
+        .width(Length::Units(500))
+        .into()
 }
 
 pub fn update_spend_view<'a>(
diff --git a/gui/src/app/view/spend/mod.rs b/gui/src/app/view/spend/mod.rs
index 1c9c9e6e..4a98a927 100644
--- a/gui/src/app/view/spend/mod.rs
+++ b/gui/src/app/view/spend/mod.rs
@@ -111,23 +111,63 @@ fn spend_tx_list_view<'a>(i: usize, tx: &SpendTx) -> Element<'a, Message> {
                 .push(
                     Row::new()
                         .push(badge::spend())
-                        .push_maybe(match tx.status {
-                            SpendStatus::Deprecated => Some(
-                                Container::new(text("  Deprecated  ").small())
-                                    .padding(3)
-                                    .style(badge::PillStyle::Simple),
-                            ),
-                            SpendStatus::Broadcast => Some(
-                                Container::new(text("  Broadcast  ").small())
-                                    .padding(3)
-                                    .style(badge::PillStyle::Success),
-                            ),
-                            _ => None,
+                        .push(if let Some(sigs) = tx.sigs.recovery_path() {
+                            Row::new()
+                                .spacing(10)
+                                .align_items(Alignment::Center)
+                                .push(
+                                    Row::new()
+                                        .spacing(5)
+                                        .align_items(Alignment::Center)
+                                        .push(text(format!(
+                                            "{}/{}",
+                                            if sigs.signed_pubkeys.len() <= sigs.threshold {
+                                                sigs.signed_pubkeys.len()
+                                            } else {
+                                                sigs.threshold
+                                            },
+                                            sigs.threshold
+                                        )))
+                                        .push(icon::key_icon()),
+                                )
+                                .push(
+                                    Container::new(text(" Recovery ").small())
+                                        .padding(3)
+                                        .style(badge::PillStyle::Simple),
+                                )
+                        } else {
+                            let sigs = tx.sigs.primary_path();
+                            Row::new()
+                                .spacing(5)
+                                .align_items(Alignment::Center)
+                                .push(text(format!(
+                                    "{}/{}",
+                                    if sigs.signed_pubkeys.len() <= sigs.threshold {
+                                        sigs.signed_pubkeys.len()
+                                    } else {
+                                        sigs.threshold
+                                    },
+                                    sigs.threshold
+                                )))
+                                .push(icon::key_icon())
                         })
                         .spacing(10)
                         .align_items(Alignment::Center)
                         .width(Length::Fill),
                 )
+                .push_maybe(match tx.status {
+                    SpendStatus::Deprecated => Some(
+                        Container::new(text("  Deprecated  ").small())
+                            .padding(3)
+                            .style(badge::PillStyle::Simple),
+                    ),
+                    SpendStatus::Broadcast => Some(
+                        Container::new(text("  Broadcast  ").small())
+                            .padding(3)
+                            .style(badge::PillStyle::Success),
+                    ),
+                    _ => None,
+                })
                 .push(
                     Column::new()
                         .push(amount(&tx.spend_amount))
diff --git a/gui/src/app/wallet.rs b/gui/src/app/wallet.rs
index 7fd91089..6e8bd4a2 100644
--- a/gui/src/app/wallet.rs
+++ b/gui/src/app/wallet.rs
@@ -1,16 +1,46 @@
+use std::collections::HashMap;
+
+use crate::hw::HardwareWalletConfig;
+
 use liana::descriptors::MultipathDescriptor;
+use liana::miniscript::bitcoin::util::bip32::Fingerprint;
+
+pub const DEFAULT_WALLET_NAME: &str = "Liana";
 
-#[derive(Clone)]
+#[derive(Debug, Clone)]
 pub struct Wallet {
     pub name: String,
     pub main_descriptor: MultipathDescriptor,
+    pub keys_aliases: HashMap<Fingerprint, String>,
+    pub hardware_wallets: Vec<HardwareWalletConfig>,
 }
 
 impl Wallet {
-    pub fn new(main_descriptor: MultipathDescriptor) -> Self {
+    pub fn new(name: String, main_descriptor: MultipathDescriptor) -> Self {
+        Self {
+            name,
+            main_descriptor,
+            keys_aliases: HashMap::new(),
+            hardware_wallets: Vec::new(),
+        }
+    }
+
+    pub fn legacy(main_descriptor: MultipathDescriptor) -> Self {
         Self {
-            name: "Liana".to_string(),
+            name: DEFAULT_WALLET_NAME.to_string(),
             main_descriptor,
+            keys_aliases: HashMap::new(),
+            hardware_wallets: Vec::new(),
         }
     }
+
+    pub fn with_key_aliases(mut self, aliases: HashMap<Fingerprint, String>) -> Self {
+        self.keys_aliases = aliases;
+        self
+    }
+
+    pub fn with_harware_wallets(mut self, hardware_wallets: Vec<HardwareWalletConfig>) -> Self {
+        self.hardware_wallets = hardware_wallets;
+        self
+    }
 }
diff --git a/gui/src/daemon/mod.rs b/gui/src/daemon/mod.rs
index 4ff20477..d81716eb 100644
--- a/gui/src/daemon/mod.rs
+++ b/gui/src/daemon/mod.rs
@@ -72,25 +72,29 @@ pub trait Daemon: Debug {
     fn list_txs(&self, txid: &[Txid]) -> Result<model::ListTransactionsResult, DaemonError>;
 
     fn list_spend_transactions(&self) -> Result<Vec<model::SpendTx>, DaemonError> {
+        let info = self.get_info()?;
         let coins = self.list_coins()?.coins;
-        let spend_txs = self.list_spend_txs()?.spend_txs;
-        Ok(spend_txs
-            .into_iter()
-            .map(|tx| {
-                let coins = coins
-                    .iter()
-                    .filter(|coin| {
-                        tx.psbt
-                            .unsigned_tx
-                            .input
-                            .iter()
-                            .any(|input| input.previous_output == coin.outpoint)
-                    })
-                    .copied()
-                    .collect();
-                model::SpendTx::new(tx.psbt, coins)
-            })
-            .collect())
+        let mut spend_txs = Vec::new();
+        for tx in self.list_spend_txs()?.spend_txs {
+            let coins = coins
+                .iter()
+                .filter(|coin| {
+                    tx.psbt
+                        .unsigned_tx
+                        .input
+                        .iter()
+                        .any(|input| input.previous_output == coin.outpoint)
+                })
+                .copied()
+                .collect();
+            let sigs = info
+                .descriptors
+                .main
+                .partial_spend_info(&tx.psbt)
+                .map_err(|e| DaemonError::Unexpected(e.to_string()))?;
+            spend_txs.push(model::SpendTx::new(tx.psbt, coins, sigs))
+        }
+        Ok(spend_txs)
     }
 
     fn list_history_txs(
diff --git a/gui/src/daemon/model.rs b/gui/src/daemon/model.rs
index ec14e83d..9f38582f 100644
--- a/gui/src/daemon/model.rs
+++ b/gui/src/daemon/model.rs
@@ -3,6 +3,7 @@ pub use liana::{
         CreateSpendResult, GetAddressResult, GetInfoResult, ListCoinsEntry, ListCoinsResult,
         ListSpendEntry, ListSpendResult, ListTransactionsResult, TransactionInfo,
     },
+    descriptors::PartialSpendInfo,
     miniscript::bitcoin::{util::psbt::Psbt, Amount, Transaction},
 };
 
@@ -28,6 +29,7 @@ pub struct SpendTx {
     pub spend_amount: Amount,
     pub fee_amount: Amount,
     pub status: SpendStatus,
+    pub sigs: PartialSpendInfo,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq)]
@@ -38,7 +40,7 @@ pub enum SpendStatus {
 }
 
 impl SpendTx {
-    pub fn new(psbt: Psbt, coins: Vec<Coin>) -> Self {
+    pub fn new(psbt: Psbt, coins: Vec<Coin>, sigs: PartialSpendInfo) -> Self {
         let mut change_indexes = Vec::new();
         let (change_amount, spend_amount) = psbt.unsigned_tx.output.iter().enumerate().fold(
             (Amount::from_sat(0), Amount::from_sat(0)),
@@ -72,11 +74,21 @@ impl SpendTx {
             spend_amount,
             fee_amount: inputs_amount - spend_amount - change_amount,
             status,
+            sigs,
         }
     }
 
-    pub fn is_signed(&self) -> bool {
-        !self.psbt.inputs.first().unwrap().partial_sigs.is_empty()
+    pub fn is_ready(&self) -> bool {
+        let path = self.sigs.primary_path();
+        if path.signed_pubkeys.len() >= path.threshold {
+            return true;
+        }
+        if let Some(path) = self.sigs.recovery_path() {
+            if path.signed_pubkeys.len() >= path.threshold {
+                return true;
+            }
+        }
+        false
     }
 }
 
diff --git a/gui/src/installer/config.rs b/gui/src/installer/config.rs
index 696ab269..a4dd53af 100644
--- a/gui/src/installer/config.rs
+++ b/gui/src/installer/config.rs
@@ -2,7 +2,7 @@ use std::convert::TryFrom;
 
 use liana::config::Config as LianaConfig;
 
-use super::step::Context;
+use super::Context;
 
 pub const DEFAULT_FILE_NAME: &str = "daemon.toml";
 
diff --git a/gui/src/installer/context.rs b/gui/src/installer/context.rs
new file mode 100644
index 00000000..91b0d687
--- /dev/null
+++ b/gui/src/installer/context.rs
@@ -0,0 +1,87 @@
+use std::path::PathBuf;
+use std::time::Duration;
+
+use crate::{
+    app::{
+        settings::{KeySetting, Settings, WalletSetting},
+        wallet::DEFAULT_WALLET_NAME,
+    },
+    hw::HardwareWalletConfig,
+};
+use async_hwi::DeviceKind;
+use liana::{
+    config::Config,
+    config::{BitcoinConfig, BitcoindConfig},
+    descriptors::MultipathDescriptor,
+    miniscript::bitcoin,
+};
+
+#[derive(Clone)]
+pub struct Context {
+    pub bitcoin_config: BitcoinConfig,
+    pub bitcoind_config: Option<BitcoindConfig>,
+    pub descriptor: Option<MultipathDescriptor>,
+    pub keys: Vec<KeySetting>,
+    pub hws: Vec<(
+        DeviceKind,
+        bitcoin::util::bip32::Fingerprint,
+        Option<[u8; 32]>,
+    )>,
+    pub data_dir: PathBuf,
+}
+
+impl Context {
+    pub fn new(network: bitcoin::Network, data_dir: PathBuf) -> Self {
+        Self {
+            bitcoin_config: BitcoinConfig {
+                network,
+                poll_interval_secs: Duration::from_secs(30),
+            },
+            hws: Vec::new(),
+            keys: Vec::new(),
+            bitcoind_config: None,
+            descriptor: None,
+            data_dir,
+        }
+    }
+
+    pub fn extract_gui_settings(&self) -> Settings {
+        let hardware_wallets = self
+            .hws
+            .iter()
+            .filter_map(|(kind, fingerprint, token)| {
+                token
+                    .as_ref()
+                    .map(|token| HardwareWalletConfig::new(kind, fingerprint, token))
+            })
+            .collect();
+        Settings {
+            wallets: vec![WalletSetting {
+                name: DEFAULT_WALLET_NAME.to_string(),
+                descriptor_checksum: self
+                    .descriptor
+                    .as_ref()
+                    .unwrap()
+                    .to_string()
+                    .split_once('#')
+                    .map(|(_, checksum)| checksum)
+                    .unwrap()
+                    .to_string(),
+                keys: self.keys.clone(),
+                hardware_wallets,
+            }],
+        }
+    }
+
+    pub fn extract_daemon_config(&self) -> Config {
+        Config {
+            #[cfg(unix)]
+            daemon: false,
+            log_level: log::LevelFilter::Info,
+            main_descriptor: self.descriptor.clone().unwrap(),
+            data_dir: Some(self.data_dir.clone()),
+            bitcoin_config: self.bitcoin_config.clone(),
+            bitcoind_config: self.bitcoind_config.clone(),
+        }
+    }
+}
diff --git a/gui/src/installer/message.rs b/gui/src/installer/message.rs
index 880b632b..ea2388a4 100644
--- a/gui/src/installer/message.rs
+++ b/gui/src/installer/message.rs
@@ -1,4 +1,7 @@
-use liana::miniscript::bitcoin::{util::bip32::Fingerprint, Network};
+use liana::miniscript::{
+    bitcoin::{util::bip32::Fingerprint, Network},
+    DescriptorPublicKey,
+};
 use std::path::PathBuf;
 
 use super::Error;
@@ -7,8 +10,9 @@ use crate::hw::HardwareWallet;
 #[derive(Debug, Clone)]
 pub enum Message {
     CreateWallet,
+    ParticipateWallet,
     ImportWallet,
-    BackupDone(bool),
+    UserActionDone(bool),
     Exit(PathBuf),
     Clibpboard(String),
     Next,
@@ -21,6 +25,7 @@ pub enum Message {
     Network(Network),
     DefineBitcoind(DefineBitcoind),
     DefineDescriptor(DefineDescriptor),
+    ImportXpub(usize, Result<DescriptorPublicKey, Error>),
     ConnectedHardwareWallets(Vec<HardwareWallet>),
     WalletRegistered(Result<(Fingerprint, Option<[u8; 32]>), Error>),
 }
@@ -34,10 +39,22 @@ pub enum DefineBitcoind {
 #[derive(Debug, Clone)]
 pub enum DefineDescriptor {
     ImportDescriptor(String),
-    ImportUserHWXpub,
-    ImportHeirHWXpub,
-    XpubImported(Result<String, Error>),
-    UserXpubEdited(String),
-    HeirXpubEdited(String),
+    /// AddKey(is_recovery)
+    AddKey(bool),
+    Key(bool, usize, DefineKey),
+    HWXpubImported(Result<DescriptorPublicKey, Error>),
+    XPubEdited(String),
+    EditName,
+    NameEdited(String),
     SequenceEdited(String),
+    ThresholdEdited(bool, usize),
+    ConfirmXpub,
+}
+
+#[derive(Debug, Clone)]
+pub enum DefineKey {
+    Delete,
+    Edit,
+    Clipboard(String),
+    Edited(String, DescriptorPublicKey),
 }
diff --git a/gui/src/installer/mod.rs b/gui/src/installer/mod.rs
index 76630970..dc87d294 100644
--- a/gui/src/installer/mod.rs
+++ b/gui/src/installer/mod.rs
@@ -1,4 +1,4 @@
-mod config;
+mod context;
 mod message;
 mod prompt;
 mod step;
@@ -7,17 +7,15 @@ mod view;
 use iced::{clipboard, Command, Element, Subscription};
 use liana::miniscript::bitcoin;
 
-use std::convert::TryInto;
+use context::Context;
 use std::io::Write;
 use std::path::PathBuf;
 
-use crate::{
-    app::config as gui_config, hw::HardwareWalletConfig, installer::config::DEFAULT_FILE_NAME,
-};
+use crate::app::{config as gui_config, settings as gui_settings};
 
 pub use message::Message;
 use step::{
-    BackupDescriptor, Context, DefineBitcoind, DefineDescriptor, Final, ImportDescriptor,
+    BackupDescriptor, DefineBitcoind, DefineDescriptor, Final, ImportDescriptor, ParticipateXpub,
     RegisterDescriptor, Step, Welcome,
 };
 
@@ -100,10 +98,22 @@ impl Installer {
                 ];
                 self.next()
             }
+            Message::ParticipateWallet => {
+                self.steps = vec![
+                    Welcome::default().into(),
+                    ParticipateXpub::new().into(),
+                    ImportDescriptor::new(false).into(),
+                    BackupDescriptor::default().into(),
+                    RegisterDescriptor::default().into(),
+                    DefineBitcoind::new().into(),
+                    Final::new().into(),
+                ];
+                self.next()
+            }
             Message::ImportWallet => {
                 self.steps = vec![
                     Welcome::default().into(),
-                    ImportDescriptor::new().into(),
+                    ImportDescriptor::new(true).into(),
                     RegisterDescriptor::default().into(),
                     DefineBitcoind::new().into(),
                     Final::new().into(),
@@ -152,19 +162,7 @@ impl Installer {
 }
 
 pub async fn install(ctx: Context) -> Result<PathBuf, Error> {
-    let hardware_wallets = ctx
-        .hws
-        .iter()
-        .filter_map(|(kind, fingerprint, token)| {
-            token
-                .as_ref()
-                .map(|token| HardwareWalletConfig::new(kind, fingerprint, token))
-        })
-        .collect();
-
-    let mut cfg: liana::config::Config = ctx
-        .try_into()
-        .expect("Everything should be checked at this point");
+    let mut cfg: liana::config::Config = ctx.extract_daemon_config();
     // Start Daemon to check correctness of installation
     let daemon = liana::DaemonHandle::start_default(cfg.clone()).map_err(|e| {
         Error::Unexpected(format!("Failed to start daemon with entered config: {}", e))
@@ -179,42 +177,55 @@ pub async fn install(ctx: Context) -> Result<PathBuf, Error> {
     let mut datadir_path = cfg.data_dir.clone().unwrap();
     datadir_path.push(cfg.bitcoin_config.network.to_string());
 
-    // create lianad configuration file
-    let mut daemon_config_path = datadir_path.clone();
-    daemon_config_path.push(DEFAULT_FILE_NAME);
-    let mut daemon_config_file = std::fs::File::create(&daemon_config_path)
-        .map_err(|e| Error::CannotCreateFile(e.to_string()))?;
-
     // Step needed because of ValueAfterTable error in the toml serialize implementation.
     let daemon_config =
         toml::Value::try_from(&cfg).expect("daemon::Config has a proper Serialize implementation");
 
-    daemon_config_file
-        .write_all(daemon_config.to_string().as_bytes())
-        .map_err(|e| Error::CannotWriteToFile(e.to_string()))?;
+    // create lianad configuration file
+    let daemon_config_path = create_and_write_file(
+        datadir_path.clone(),
+        "daemon.toml",
+        daemon_config.to_string().as_bytes(),
+    )?;
 
     // create liana GUI configuration file
-    let mut gui_config_path = datadir_path;
-    gui_config_path.push(gui_config::DEFAULT_FILE_NAME);
-    let mut gui_config_file = std::fs::File::create(&gui_config_path)
-        .map_err(|e| Error::CannotCreateFile(e.to_string()))?;
-
-    gui_config_file
-        .write_all(
-            toml::to_string(&gui_config::Config::new(
-                daemon_config_path.canonicalize().map_err(|e| {
-                    Error::Unexpected(format!("Failed to canonicalize daemon config path: {}", e))
-                })?,
-                hardware_wallets,
-            ))
-            .unwrap()
-            .as_bytes(),
-        )
-        .map_err(|e| Error::CannotWriteToFile(e.to_string()))?;
+    let gui_config_path = create_and_write_file(
+        datadir_path.clone(),
+        gui_config::DEFAULT_FILE_NAME,
+        toml::to_string(&gui_config::Config::new(
+            daemon_config_path.canonicalize().map_err(|e| {
+                Error::Unexpected(format!("Failed to canonicalize daemon config path: {}", e))
+            })?,
+        ))
+        .unwrap()
+        .as_bytes(),
+    )?;
+
+    // create liana GUI settings file
+    let settings: gui_settings::Settings = ctx.extract_gui_settings();
+    create_and_write_file(
+        datadir_path,
+        gui_settings::DEFAULT_FILE_NAME,
+        serde_json::to_string_pretty(&settings).unwrap().as_bytes(),
+    )?;
 
     Ok(gui_config_path)
 }
 
+pub fn create_and_write_file(
+    mut network_datadir: PathBuf,
+    file_name: &str,
+    data: &[u8],
+) -> Result<PathBuf, Error> {
+    network_datadir.push(file_name);
+    let path = network_datadir;
+    let mut file =
+        std::fs::File::create(&path).map_err(|e| Error::CannotCreateFile(e.to_string()))?;
+    file.write_all(data)
+        .map_err(|e| Error::CannotWriteToFile(e.to_string()))?;
+    Ok(path)
+}
+
 #[derive(Debug, Clone)]
 pub enum Error {
     CannotCreateDatadir(String),
diff --git a/gui/src/installer/prompt.rs b/gui/src/installer/prompt.rs
index c84211c9..7d91ea48 100644
--- a/gui/src/installer/prompt.rs
+++ b/gui/src/installer/prompt.rs
@@ -1,2 +1,8 @@
 pub const BACKUP_DESCRIPTOR_MESSAGE: &str = "The descriptor is necessary to recover your funds. The backup of your key (via mnemonics, sometimes called 'seed words') is not enough. Please make sure you have backed up both your private key and your descriptor.";
 pub const BACKUP_DESCRIPTOR_HELP: &str = "In Bitcoin, the coins are locked using a Script (related to the 'address'). In order to recover your funds you need both to know the Scripts you have participated in (your 'addresses'), and be able to sign a transaction that spends from those. For the ability to sign you backup your private key, this is your mnemonics ('seed words'). For finding the coins that belongs to you you backup a template of your Script ( / 'addresses'), this is your descriptor. Note however the descriptor needs not be as securely stored as the private key. A thief that steals your descriptor but not your private key will not be able to steal your funds.";
+pub const DEFINE_DESCRIPTOR_PRIMATRY_PATH_TOOLTIP: &str =
+    "This is the keys that can spend received coins immediately,\n with no time restriction.";
+pub const DEFINE_DESCRIPTOR_SEQUENCE_TOOLTIP: &str =
+    "Number of blocks after a coin is received \nfor which the recovery path is not available";
+pub const DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP: &str =
+    "The alias is applied on all the keys derived from the same seed";
diff --git a/gui/src/installer/step/descriptor.rs b/gui/src/installer/step/descriptor.rs
index 986de404..bf4048e4 100644
--- a/gui/src/installer/step/descriptor.rs
+++ b/gui/src/installer/step/descriptor.rs
@@ -1,39 +1,50 @@
+use std::collections::{HashMap, HashSet};
 use std::path::PathBuf;
 use std::str::FromStr;
 
 use iced::{Command, Element};
 use liana::{
-    descriptors::MultipathDescriptor,
+    descriptors::{LianaDescKeys, MultipathDescriptor},
     miniscript::{
         bitcoin::{
-            util::bip32::{DerivationPath, ExtendedPubKey, Fingerprint},
+            util::bip32::{ChildNumber, DerivationPath, Fingerprint},
             Network,
         },
-        descriptor::DescriptorPublicKey,
+        descriptor::{DerivPaths, DescriptorMultiXKey, DescriptorPublicKey, Wildcard},
     },
 };
 
 use crate::{
+    app::settings::KeySetting,
     hw::{list_hardware_wallets, HardwareWallet},
     installer::{
         message::{self, Message},
         step::{Context, Step},
         view, Error,
     },
-    ui::component::form,
+    ui::component::{form, modal::Modal},
 };
 
-const LIANA_STANDARD_PATH: &str = "m/48'/0'/0'/2'";
-const LIANA_TESTNET_STANDARD_PATH: &str = "m/48'/1'/0'/2'";
+pub trait DescriptorKeyModal {
+    fn processing(&self) -> bool {
+        false
+    }
+    fn update(&mut self, _message: Message) -> Command<Message> {
+        Command::none()
+    }
+    fn view(&self) -> Element<Message>;
+}
 
 pub struct DefineDescriptor {
     network: Network,
     network_valid: bool,
     data_dir: Option<PathBuf>,
-    user_xpub: form::Value<String>,
-    heir_xpub: form::Value<String>,
+    spending_keys: Vec<DescriptorKey>,
+    spending_threshold: usize,
+    recovery_keys: Vec<DescriptorKey>,
+    recovery_threshold: usize,
     sequence: form::Value<String>,
-    modal: Option<GetHardwareWalletXpubModal>,
+    modal: Option<Box<dyn DescriptorKeyModal>>,
 
     error: Option<String>,
 }
@@ -44,19 +55,146 @@ impl DefineDescriptor {
             network: Network::Bitcoin,
             data_dir: None,
             network_valid: true,
-            user_xpub: form::Value::default(),
-            heir_xpub: form::Value::default(),
+            spending_keys: vec![DescriptorKey::default()],
+            spending_threshold: 1,
+            recovery_keys: vec![DescriptorKey::default()],
+            recovery_threshold: 1,
             sequence: form::Value::default(),
             modal: None,
             error: None,
         }
     }
+
+    fn valid(&self) -> bool {
+        !self.spending_keys.is_empty()
+            && !self.recovery_keys.is_empty()
+            && !self.sequence.value.is_empty()
+            && !self.spending_keys.iter().any(|k| k.key.is_none())
+            && !self.spending_keys.iter().any(|k| k.key.is_none())
+    }
+
+    // TODO: Improve algo
+    // Mark as duplicate every defined key that have the same name but not the same fingerprint.
+    // And every undefined_key that have a same name than an other key.
+    fn check_for_duplicate(&mut self) {
+        let mut all_keys = HashSet::new();
+        let mut duplicate_keys = HashSet::new();
+        let mut all_names: HashMap<String, Fingerprint> = HashMap::new();
+        let mut duplicate_names = HashSet::new();
+        for spending_key in &self.spending_keys {
+            if let Some(key) = &spending_key.key {
+                if let Some(fg) = all_names.get(&spending_key.name) {
+                    if fg != &key.master_fingerprint() {
+                        duplicate_names.insert(spending_key.name.clone());
+                    }
+                } else {
+                    all_names.insert(spending_key.name.clone(), key.master_fingerprint());
+                }
+                if all_keys.contains(key) {
+                    duplicate_keys.insert(key.clone());
+                } else {
+                    all_keys.insert(key.clone());
+                }
+            }
+        }
+        for recovery_key in &self.recovery_keys {
+            if let Some(key) = &recovery_key.key {
+                if let Some(fg) = all_names.get(&recovery_key.name) {
+                    if fg != &key.master_fingerprint() {
+                        duplicate_names.insert(recovery_key.name.clone());
+                    }
+                } else {
+                    all_names.insert(recovery_key.name.clone(), key.master_fingerprint());
+                }
+                if all_keys.contains(key) {
+                    duplicate_keys.insert(key.clone());
+                } else {
+                    all_keys.insert(key.clone());
+                }
+            }
+        }
+        for spending_key in self.spending_keys.iter_mut() {
+            spending_key.duplicate_name = duplicate_names.contains(&spending_key.name);
+            if let Some(key) = &spending_key.key {
+                spending_key.duplicate_key = duplicate_keys.contains(key);
+            }
+        }
+        for recovery_key in self.recovery_keys.iter_mut() {
+            if let Some(key) = &recovery_key.key {
+                recovery_key.duplicate_key = duplicate_keys.contains(key);
+            }
+        }
+    }
+
+    fn edit_alias_for_key_with_same_fingerprint(&mut self, name: String, fingerprint: Fingerprint) {
+        for spending_key in &mut self.spending_keys {
+            if spending_key.key.as_ref().map(|k| k.master_fingerprint()) == Some(fingerprint) {
+                spending_key.name = name.clone();
+            }
+        }
+        for recovery_key in &mut self.recovery_keys {
+            if recovery_key.key.as_ref().map(|k| k.master_fingerprint()) == Some(fingerprint) {
+                recovery_key.name = name.clone();
+            }
+        }
+    }
+
+    /// Returns the maximum account index per key fingerprint
+    fn fingerprint_account_index_mappping(&self) -> HashMap<Fingerprint, ChildNumber> {
+        let mut mapping = HashMap::new();
+        let update_mapping =
+            |keys: &[DescriptorKey], mapping: &mut HashMap<Fingerprint, ChildNumber>| {
+                for key in keys {
+                    if let Some(DescriptorPublicKey::MultiXPub(key)) = key.key.as_ref() {
+                        if let Some((fingerprint, derivation_path)) = key.origin.as_ref() {
+                            let index = if derivation_path.len() >= 4 {
+                                if derivation_path[0].to_string() == "48'" {
+                                    Some(derivation_path[2])
+                                } else {
+                                    None
+                                }
+                            } else {
+                                None
+                            };
+                            if let Some(index) = index {
+                                if let Some(previous_index) = mapping.get(fingerprint) {
+                                    if index > *previous_index {
+                                        mapping.insert(*fingerprint, index);
+                                    }
+                                } else {
+                                    mapping.insert(*fingerprint, index);
+                                }
+                            }
+                        }
+                    }
+                }
+            };
+        update_mapping(&self.spending_keys, &mut mapping);
+        update_mapping(&self.recovery_keys, &mut mapping);
+        mapping
+    }
+
+    fn keys_aliases(&self) -> HashMap<Fingerprint, String> {
+        let mut map = HashMap::new();
+        for spending_key in &self.spending_keys {
+            if let Some(key) = spending_key.key.as_ref() {
+                map.insert(key.master_fingerprint(), spending_key.name.clone());
+            }
+        }
+        for recovery_key in &self.recovery_keys {
+            if let Some(key) = recovery_key.key.as_ref() {
+                map.insert(key.master_fingerprint(), recovery_key.name.clone());
+            }
+        }
+        map
+    }
 }
 
 impl Step for DefineDescriptor {
     // 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, message: Message) -> Command<Message> {
+        self.error = None;
         match message {
             Message::Close => {
                 self.modal = None;
@@ -66,18 +204,21 @@ impl Step for DefineDescriptor {
                 let mut network_datadir = self.data_dir.clone().unwrap();
                 network_datadir.push(self.network.to_string());
                 self.network_valid = !network_datadir.exists();
+                for key in self.spending_keys.iter_mut() {
+                    key.check_network(self.network);
+                }
+                for key in self.recovery_keys.iter_mut() {
+                    key.check_network(self.network);
+                }
             }
             Message::DefineDescriptor(msg) => {
                 match msg {
-                    message::DefineDescriptor::UserXpubEdited(xpub) => {
-                        self.user_xpub.value = xpub;
-                        self.user_xpub.valid = true;
-                        self.modal = None;
-                    }
-                    message::DefineDescriptor::HeirXpubEdited(xpub) => {
-                        self.heir_xpub.value = xpub;
-                        self.heir_xpub.valid = true;
-                        self.modal = None;
+                    message::DefineDescriptor::ThresholdEdited(is_recovery, value) => {
+                        if is_recovery {
+                            self.recovery_threshold = value;
+                        } else {
+                            self.spending_threshold = value;
+                        }
                     }
                     message::DefineDescriptor::SequenceEdited(seq) => {
                         self.sequence.valid = true;
@@ -85,18 +226,98 @@ impl Step for DefineDescriptor {
                             self.sequence.value = seq;
                         }
                     }
-                    message::DefineDescriptor::ImportUserHWXpub => {
-                        let modal = GetHardwareWalletXpubModal::new(false, self.network);
-                        let cmd = modal.load();
-                        self.modal = Some(modal);
-                        return cmd;
-                    }
-                    message::DefineDescriptor::ImportHeirHWXpub => {
-                        let modal = GetHardwareWalletXpubModal::new(true, self.network);
-                        let cmd = modal.load();
-                        self.modal = Some(modal);
-                        return cmd;
+                    message::DefineDescriptor::AddKey(is_recovery) => {
+                        if is_recovery {
+                            self.recovery_keys.push(DescriptorKey::default());
+                            self.recovery_threshold += 1;
+                        } else {
+                            self.spending_keys.push(DescriptorKey::default());
+                            self.spending_threshold += 1;
+                        }
                     }
+                    message::DefineDescriptor::Key(is_recovery, i, msg) => match msg {
+                        message::DefineKey::Clipboard(key) => {
+                            return Command::perform(async move { key }, Message::Clibpboard);
+                        }
+                        message::DefineKey::Edited(name, imported_key) => {
+                            self.edit_alias_for_key_with_same_fingerprint(
+                                name.clone(),
+                                imported_key.master_fingerprint(),
+                            );
+                            if is_recovery {
+                                if let Some(recovery_key) = self.recovery_keys.get_mut(i) {
+                                    recovery_key.name = name;
+                                    recovery_key.key = Some(imported_key);
+                                    recovery_key.check_network(self.network);
+                                }
+                            } else if let Some(spending_key) = self.spending_keys.get_mut(i) {
+                                spending_key.name = name;
+                                spending_key.key = Some(imported_key);
+                                spending_key.check_network(self.network);
+                            }
+                            self.modal = None;
+                            self.check_for_duplicate();
+                        }
+                        message::DefineKey::Edit => {
+                            if is_recovery {
+                                if let Some(recovery_key) = self.recovery_keys.get(i) {
+                                    let name = recovery_key.name.clone();
+                                    let key = recovery_key
+                                        .key
+                                        .as_ref()
+                                        .map(|k| {
+                                            k.to_string().trim_end_matches("/<0;1>/*").to_string()
+                                        })
+                                        .unwrap_or_else(|| "".to_string());
+                                    let modal = EditXpubModal::new(
+                                        name,
+                                        key,
+                                        i,
+                                        is_recovery,
+                                        self.network,
+                                        self.fingerprint_account_index_mappping(),
+                                        self.keys_aliases(),
+                                    );
+                                    let cmd = modal.load();
+                                    self.modal = Some(Box::new(modal));
+                                    return cmd;
+                                }
+                            } else if let Some(spending_key) = self.spending_keys.get(i) {
+                                let name = spending_key.name.clone();
+                                let key = spending_key
+                                    .key
+                                    .as_ref()
+                                    .map(|k| k.to_string().trim_end_matches("/<0;1>/*").to_string())
+                                    .unwrap_or_else(|| "".to_string());
+                                let modal = EditXpubModal::new(
+                                    name,
+                                    key,
+                                    i,
+                                    is_recovery,
+                                    self.network,
+                                    self.fingerprint_account_index_mappping(),
+                                    self.keys_aliases(),
+                                );
+                                let cmd = modal.load();
+                                self.modal = Some(Box::new(modal));
+                                return cmd;
+                            }
+                        }
+                        message::DefineKey::Delete => {
+                            if is_recovery {
+                                self.recovery_keys.remove(i);
+                                if self.recovery_threshold > self.recovery_keys.len() {
+                                    self.recovery_threshold -= 1;
+                                }
+                            } else {
+                                self.spending_keys.remove(i);
+                                if self.spending_threshold > self.spending_keys.len() {
+                                    self.spending_threshold -= 1;
+                                }
+                            }
+                            self.check_for_duplicate();
+                        }
+                    },
                     _ => {
                         if let Some(modal) = &mut self.modal {
                             return modal.update(Message::DefineDescriptor(msg));
@@ -123,57 +344,163 @@ impl Step for DefineDescriptor {
 
     fn apply(&mut self, ctx: &mut Context) -> bool {
         ctx.bitcoin_config.network = self.network;
-        // descriptor forms for import or creation cannot be both empty or filled.
-        let user_key = DescriptorPublicKey::from_str(&format!("{}/<0;1>/*", &self.user_xpub.value));
-        self.user_xpub.valid = user_key.is_ok();
-        if let Ok(key) = &user_key {
-            self.user_xpub.valid = check_key_network(key, self.network);
+        ctx.keys = Vec::new();
+        let mut spending_keys: Vec<DescriptorPublicKey> = Vec::new();
+        for spending_key in self.spending_keys.iter().clone() {
+            if let Some(key) = spending_key.key.as_ref() {
+                if let DescriptorPublicKey::MultiXPub(xpub) = key {
+                    if let Some((master_fingerprint, _)) = xpub.origin {
+                        ctx.keys.push(KeySetting {
+                            master_fingerprint,
+                            name: spending_key.name.clone(),
+                        });
+                    }
+                }
+                spending_keys.push(key.clone());
+            }
         }
 
-        let heir_key = DescriptorPublicKey::from_str(&format!("{}/<0;1>/*", &self.heir_xpub.value));
-        self.heir_xpub.valid = heir_key.is_ok();
-        if let Ok(key) = &heir_key {
-            self.heir_xpub.valid = check_key_network(key, self.network);
+        let mut recovery_keys: Vec<DescriptorPublicKey> = Vec::new();
+        for recovery_key in self.recovery_keys.iter().clone() {
+            if let Some(key) = recovery_key.key.as_ref() {
+                if let DescriptorPublicKey::MultiXPub(xpub) = key {
+                    if let Some((master_fingerprint, _)) = xpub.origin {
+                        ctx.keys.push(KeySetting {
+                            master_fingerprint,
+                            name: recovery_key.name.clone(),
+                        });
+                    }
+                }
+                recovery_keys.push(key.clone());
+            }
         }
 
         let sequence = self.sequence.value.parse::<u16>();
         self.sequence.valid = sequence.is_ok();
 
         if !self.network_valid
-            || !self.user_xpub.valid
-            || !self.heir_xpub.valid
             || !self.sequence.valid
+            || recovery_keys.is_empty()
+            || spending_keys.is_empty()
         {
             return false;
         }
 
-        let desc =
-            match MultipathDescriptor::new(user_key.unwrap(), heir_key.unwrap(), sequence.unwrap())
-            {
-                Ok(desc) => desc,
+        let spending_keys = if spending_keys.len() == 1 {
+            LianaDescKeys::from_single(spending_keys[0].clone())
+        } else {
+            match LianaDescKeys::from_multi(self.spending_threshold, spending_keys) {
+                Ok(keys) => keys,
                 Err(e) => {
                     self.error = Some(e.to_string());
                     return false;
                 }
-            };
+            }
+        };
+
+        let recovery_keys = if recovery_keys.len() == 1 {
+            LianaDescKeys::from_single(recovery_keys[0].clone())
+        } else {
+            match LianaDescKeys::from_multi(self.recovery_threshold, recovery_keys) {
+                Ok(keys) => keys,
+                Err(e) => {
+                    self.error = Some(e.to_string());
+                    return false;
+                }
+            }
+        };
+
+        let desc = match MultipathDescriptor::new(spending_keys, recovery_keys, sequence.unwrap()) {
+            Ok(desc) => desc,
+            Err(e) => {
+                self.error = Some(e.to_string());
+                return false;
+            }
+        };
 
         ctx.descriptor = Some(desc);
         true
     }
 
     fn view(&self, progress: (usize, usize)) -> Element<Message> {
+        let content = view::define_descriptor(
+            progress,
+            self.network,
+            self.network_valid,
+            self.spending_keys
+                .iter()
+                .enumerate()
+                .map(|(i, key)| {
+                    key.view().map(move |msg| {
+                        Message::DefineDescriptor(message::DefineDescriptor::Key(false, i, msg))
+                    })
+                })
+                .collect(),
+            self.recovery_keys
+                .iter()
+                .enumerate()
+                .map(|(i, key)| {
+                    key.view().map(move |msg| {
+                        Message::DefineDescriptor(message::DefineDescriptor::Key(true, i, msg))
+                    })
+                })
+                .collect(),
+            &self.sequence,
+            self.spending_threshold,
+            self.recovery_threshold,
+            self.valid(),
+            self.error.as_ref(),
+        );
         if let Some(modal) = &self.modal {
-            modal.view()
+            Modal::new(content, modal.view())
+                .on_blur(if modal.processing() {
+                    None
+                } else {
+                    Some(Message::Close)
+                })
+                .into()
         } else {
-            view::define_descriptor(
-                progress,
-                self.network,
-                self.network_valid,
-                &self.user_xpub,
-                &self.heir_xpub,
-                &self.sequence,
-                self.error.as_ref(),
-            )
+            content
+        }
+    }
+}
+
+pub struct DescriptorKey {
+    pub name: String,
+    pub valid: bool,
+    pub key: Option<DescriptorPublicKey>,
+    pub duplicate_key: bool,
+    pub duplicate_name: bool,
+}
+
+impl Default for DescriptorKey {
+    fn default() -> Self {
+        Self {
+            name: "".to_string(),
+            valid: true,
+            key: None,
+            duplicate_key: false,
+            duplicate_name: false,
+        }
+    }
+}
+
+impl DescriptorKey {
+    pub fn check_network(&mut self, network: Network) {
+        if let Some(key) = &self.key {
+            self.valid = check_key_network(key, network);
+        }
+    }
+
+    pub fn view(&self) -> Element<message::DefineKey> {
+        match &self.key {
+            None => view::undefined_descriptor_key(),
+            Some(_) => view::defined_descriptor_key(
+                &self.name,
+                self.valid,
+                self.duplicate_key,
+                self.duplicate_name,
+            ),
         }
     }
 }
@@ -210,24 +537,53 @@ impl From<DefineDescriptor> for Box<dyn Step> {
     }
 }
 
-pub struct GetHardwareWalletXpubModal {
-    is_heir: bool,
-    chosen_hw: Option<usize>,
+pub struct EditXpubModal {
+    is_recovery: bool,
+    key_index: usize,
+    network: Network,
+    error: Option<Error>,
     processing: bool,
+
+    keys_aliases: HashMap<Fingerprint, String>,
+    account_indexes: HashMap<Fingerprint, ChildNumber>,
+
+    form_name: form::Value<String>,
+    form_xpub: form::Value<String>,
+    edit_name: bool,
+
+    chosen_hw: Option<usize>,
     hws: Vec<HardwareWallet>,
-    error: Option<Error>,
-    network: Network,
 }
 
-impl GetHardwareWalletXpubModal {
-    fn new(is_heir: bool, network: Network) -> Self {
+impl EditXpubModal {
+    fn new(
+        name: String,
+        key: String,
+        key_index: usize,
+        is_recovery: bool,
+        network: Network,
+        account_indexes: HashMap<Fingerprint, ChildNumber>,
+        keys_aliases: HashMap<Fingerprint, String>,
+    ) -> Self {
         Self {
-            is_heir,
+            form_name: form::Value {
+                valid: true,
+                value: name,
+            },
+            form_xpub: form::Value {
+                valid: true,
+                value: key,
+            },
+            keys_aliases,
+            account_indexes,
+            is_recovery,
+            key_index,
             chosen_hw: None,
             processing: false,
             hws: Vec::new(),
             error: None,
             network,
+            edit_name: false,
         }
     }
     fn load(&self) -> Command<Message> {
@@ -236,6 +592,13 @@ impl GetHardwareWalletXpubModal {
             Message::ConnectedHardwareWallets,
         )
     }
+}
+
+impl DescriptorKeyModal for EditXpubModal {
+    fn processing(&self) -> bool {
+        self.processing
+    }
+
     fn update(&mut self, message: Message) -> Command<Message> {
         match message {
             Message::Select(i) => {
@@ -243,11 +606,17 @@ impl GetHardwareWalletXpubModal {
                     let device = hw.device.clone();
                     self.chosen_hw = Some(i);
                     self.processing = true;
+                    // If another account n exists, the key is retrieved for the account n+1
+                    let account_index = self
+                        .account_indexes
+                        .get(&hw.fingerprint)
+                        .map(|account_index| account_index.increment().unwrap())
+                        .unwrap_or_else(|| ChildNumber::from_hardened_idx(0).unwrap());
                     return Command::perform(
-                        get_extended_pubkey(device, hw.fingerprint, self.network),
+                        get_extended_pubkey(device, hw.fingerprint, self.network, account_index),
                         |res| {
-                            Message::DefineDescriptor(message::DefineDescriptor::XpubImported(
-                                res.map(|key| key.to_string()),
+                            Message::DefineDescriptor(message::DefineDescriptor::HWXpubImported(
+                                res,
                             ))
                         },
                     );
@@ -259,98 +628,311 @@ impl GetHardwareWalletXpubModal {
             Message::Reload => {
                 return self.load();
             }
-            Message::DefineDescriptor(message::DefineDescriptor::XpubImported(res)) => {
+            Message::DefineDescriptor(message::DefineDescriptor::HWXpubImported(res)) => {
                 self.processing = false;
                 match res {
                     Ok(key) => {
-                        if self.is_heir {
-                            return Command::perform(
-                                async move { key },
-                                message::DefineDescriptor::HeirXpubEdited,
-                            )
-                            .map(Message::DefineDescriptor);
+                        if let Some(alias) = self.keys_aliases.get(&key.master_fingerprint()) {
+                            self.form_name.valid = true;
+                            self.form_name.value = alias.clone();
+                            self.edit_name = false;
                         } else {
-                            return Command::perform(
-                                async move { key },
-                                message::DefineDescriptor::UserXpubEdited,
-                            )
-                            .map(Message::DefineDescriptor);
+                            self.edit_name = true;
                         }
+                        self.form_xpub.valid = true;
+                        self.form_xpub.value =
+                            key.to_string().trim_end_matches("/<0;1>/*").to_string();
                     }
                     Err(e) => {
                         self.error = Some(e);
                     }
                 }
             }
+            Message::DefineDescriptor(message::DefineDescriptor::EditName) => {
+                self.edit_name = true;
+            }
+            Message::DefineDescriptor(message::DefineDescriptor::NameEdited(name)) => {
+                self.form_name.valid = true;
+                self.form_name.value = name;
+            }
+            Message::DefineDescriptor(message::DefineDescriptor::XPubEdited(s)) => {
+                if let Ok(DescriptorPublicKey::MultiXPub(key)) =
+                    DescriptorPublicKey::from_str(&format!("{}/<0;1>/*", s))
+                {
+                    if let Some((fingerprint, _)) = key.origin {
+                        self.form_xpub.valid = true;
+                        if let Some(alias) = self.keys_aliases.get(&fingerprint) {
+                            self.form_name.valid = true;
+                            self.form_name.value = alias.clone();
+                            self.edit_name = false;
+                        } else {
+                            self.edit_name = true;
+                        }
+                    } else {
+                        self.form_xpub.valid = false;
+                    }
+                } else {
+                    self.form_xpub.valid = false;
+                }
+                self.form_xpub.value = s;
+            }
+            Message::DefineDescriptor(message::DefineDescriptor::ConfirmXpub) => {
+                if let Ok(key) =
+                    DescriptorPublicKey::from_str(&format!("{}/<0;1>/*", self.form_xpub.value))
+                {
+                    let key_index = self.key_index;
+                    let is_recovery = self.is_recovery;
+                    let name = self.form_name.value.clone();
+                    return Command::perform(
+                        async move { (is_recovery, key_index, key) },
+                        |(is_recovery, key_index, key)| {
+                            message::DefineDescriptor::Key(
+                                is_recovery,
+                                key_index,
+                                message::DefineKey::Edited(name, key),
+                            )
+                        },
+                    )
+                    .map(Message::DefineDescriptor);
+                }
+            }
             _ => {}
         };
         Command::none()
     }
     fn view(&self) -> Element<Message> {
-        view::hardware_wallet_xpubs_modal(
-            self.is_heir,
+        view::edit_key_modal(
+            self.network,
             &self.hws,
             self.error.as_ref(),
             self.processing,
             self.chosen_hw,
+            &self.form_xpub,
+            &self.form_name,
+            self.edit_name,
         )
     }
 }
 
-pub struct XKey {
-    origin: Option<(Fingerprint, DerivationPath)>,
-    key: ExtendedPubKey,
-}
-
-impl std::fmt::Display for XKey {
-    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
-        if let Some((ref master_id, ref master_deriv)) = self.origin {
-            std::fmt::Formatter::write_str(f, "[")?;
-            for byte in master_id.into_bytes().iter() {
-                write!(f, "{:02x}", byte)?;
-            }
-            for child in master_deriv {
-                write!(f, "/{}", child)?;
-            }
-            std::fmt::Formatter::write_str(f, "]")?;
-        }
-        self.key.fmt(f)?;
-        Ok(())
-    }
-}
-
+/// LIANA_STANDARD_PATH: m/48'/0'/0'/2';
+/// LIANA_TESTNET_STANDARD_PATH: m/48'/1'/0'/2';
 async fn get_extended_pubkey(
     hw: std::sync::Arc<dyn async_hwi::HWI + Send + Sync>,
     fingerprint: Fingerprint,
     network: Network,
-) -> Result<XKey, Error> {
-    let derivation_path = DerivationPath::from_str(if network == Network::Bitcoin {
-        LIANA_STANDARD_PATH
-    } else {
-        LIANA_TESTNET_STANDARD_PATH
+    account_index: ChildNumber,
+) -> Result<DescriptorPublicKey, Error> {
+    let derivation_path = DerivationPath::from_str(&{
+        if network == Network::Bitcoin {
+            format!("m/48'/0'/{}/2'", account_index)
+        } else {
+            format!("m/48'/1'/{}/2'", account_index)
+        }
     })
     .unwrap();
-    let key = hw
+    let xkey = hw
         .get_extended_pubkey(&derivation_path, false)
         .await
         .map_err(Error::from)?;
-    Ok(XKey {
+    Ok(DescriptorPublicKey::MultiXPub(DescriptorMultiXKey {
         origin: Some((fingerprint, derivation_path)),
-        key,
-    })
+        derivation_paths: DerivPaths::new(vec![
+            DerivationPath::from_str("m/0").unwrap(),
+            DerivationPath::from_str("m/1").unwrap(),
+        ])
+        .unwrap(),
+        wildcard: Wildcard::Unhardened,
+        xkey,
+    }))
+}
+
+pub struct HardwareWalletXpubs {
+    hw: HardwareWallet,
+    xpubs: Vec<String>,
+    processing: bool,
+    error: Option<Error>,
+    next_account: ChildNumber,
+}
+
+impl HardwareWalletXpubs {
+    fn new(hw: HardwareWallet) -> Self {
+        Self {
+            hw,
+            xpubs: Vec::new(),
+            processing: false,
+            error: None,
+            next_account: ChildNumber::from_hardened_idx(0).unwrap(),
+        }
+    }
+
+    fn update(&mut self, res: Result<DescriptorPublicKey, Error>) {
+        self.processing = false;
+        match res {
+            Err(e) => {
+                self.error = e.into();
+            }
+            Ok(xpub) => {
+                self.error = None;
+                self.next_account = self.next_account.increment().unwrap();
+                self.xpubs
+                    .push(xpub.to_string().trim_end_matches("/<0;1>/*").to_string());
+            }
+        }
+    }
+
+    fn select(&mut self, i: usize, network: Network) -> Command<Message> {
+        let device = self.hw.device.clone();
+        self.processing = true;
+        self.error = None;
+        let fingerprint = self.hw.fingerprint;
+        let next_account = self.next_account;
+        Command::perform(
+            async move {
+                (
+                    i,
+                    get_extended_pubkey(device, fingerprint, network, next_account).await,
+                )
+            },
+            |(i, res)| Message::ImportXpub(i, res),
+        )
+    }
+
+    pub fn view(&self, i: usize) -> Element<Message> {
+        view::hardware_wallet_xpubs(
+            i,
+            &self.xpubs,
+            &self.hw,
+            self.processing,
+            self.error.as_ref(),
+        )
+    }
+}
+
+pub struct ParticipateXpub {
+    network: Network,
+    network_valid: bool,
+    data_dir: Option<PathBuf>,
+
+    shared: bool,
+
+    xpubs_hw: Vec<HardwareWalletXpubs>,
+}
+
+impl ParticipateXpub {
+    pub fn new() -> Self {
+        Self {
+            network: Network::Bitcoin,
+            network_valid: true,
+            data_dir: None,
+            xpubs_hw: Vec::new(),
+            shared: false,
+        }
+    }
+}
+
+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, message: Message) -> Command<Message> {
+        match message {
+            Message::Network(network) => {
+                self.network = network;
+                let mut network_datadir = self.data_dir.clone().unwrap();
+                network_datadir.push(self.network.to_string());
+                self.network_valid = !network_datadir.exists();
+            }
+            Message::UserActionDone(shared) => self.shared = shared,
+            Message::ImportXpub(i, res) => {
+                if let Some(hw) = self.xpubs_hw.get_mut(i) {
+                    hw.update(res);
+                }
+            }
+            Message::Select(i) => {
+                if let Some(hw) = self.xpubs_hw.get_mut(i) {
+                    return hw.select(i, self.network);
+                }
+            }
+            Message::ConnectedHardwareWallets(hws) => {
+                for hw in hws {
+                    if let Some(xpub_hw) = self
+                        .xpubs_hw
+                        .iter_mut()
+                        .find(|h| h.hw.fingerprint == hw.fingerprint)
+                    {
+                        xpub_hw.hw = hw;
+                    } else {
+                        self.xpubs_hw.push(HardwareWalletXpubs::new(hw));
+                    }
+                }
+            }
+            Message::Reload => {
+                return self.load();
+            }
+            _ => {}
+        };
+        Command::none()
+    }
+
+    fn load_context(&mut self, ctx: &Context) {
+        self.network = ctx.bitcoin_config.network;
+        self.data_dir = Some(ctx.data_dir.clone());
+        let mut network_datadir = ctx.data_dir.clone();
+        network_datadir.push(self.network.to_string());
+        self.network_valid = !network_datadir.exists();
+    }
+
+    fn load(&self) -> Command<Message> {
+        Command::perform(
+            list_hardware_wallets(&[], None),
+            Message::ConnectedHardwareWallets,
+        )
+    }
+
+    fn apply(&mut self, ctx: &mut Context) -> bool {
+        ctx.bitcoin_config.network = self.network;
+        true
+    }
+
+    fn view(&self, progress: (usize, usize)) -> Element<Message> {
+        view::participate_xpub(
+            progress,
+            self.network,
+            self.network_valid,
+            self.xpubs_hw
+                .iter()
+                .enumerate()
+                .map(|(i, hw)| hw.view(i))
+                .collect(),
+            self.shared,
+        )
+    }
+}
+
+impl Default for ParticipateXpub {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl From<ParticipateXpub> for Box<dyn Step> {
+    fn from(s: ParticipateXpub) -> Box<dyn Step> {
+        Box::new(s)
+    }
 }
 
 pub struct ImportDescriptor {
     network: Network,
     network_valid: bool,
+    change_network: bool,
     data_dir: Option<PathBuf>,
     imported_descriptor: form::Value<String>,
     error: Option<String>,
 }
 
 impl ImportDescriptor {
-    pub fn new() -> Self {
+    pub fn new(change_network: bool) -> Self {
         Self {
+            change_network,
             network: Network::Bitcoin,
             network_valid: true,
             data_dir: None,
@@ -408,6 +990,7 @@ impl Step for ImportDescriptor {
     fn view(&self, progress: (usize, usize)) -> Element<Message> {
         view::import_descriptor(
             progress,
+            self.change_network,
             self.network,
             self.network_valid,
             &self.imported_descriptor,
@@ -416,12 +999,6 @@ impl Step for ImportDescriptor {
     }
 }
 
-impl Default for ImportDescriptor {
-    fn default() -> Self {
-        Self::new()
-    }
-}
-
 impl From<ImportDescriptor> for Box<dyn Step> {
     fn from(s: ImportDescriptor) -> Box<dyn Step> {
         Box::new(s)
@@ -546,7 +1123,7 @@ pub struct BackupDescriptor {
 
 impl Step for BackupDescriptor {
     fn update(&mut self, message: Message) -> Command<Message> {
-        if let Message::BackupDone(done) = message {
+        if let Message::UserActionDone(done) = message {
             self.done = done;
         }
         Command::none()
diff --git a/gui/src/installer/step/mod.rs b/gui/src/installer/step/mod.rs
index b3a20ff1..198cce20 100644
--- a/gui/src/installer/step/mod.rs
+++ b/gui/src/installer/step/mod.rs
@@ -1,21 +1,18 @@
 mod descriptor;
-pub use descriptor::{BackupDescriptor, DefineDescriptor, ImportDescriptor, RegisterDescriptor};
+pub use descriptor::{
+    BackupDescriptor, DefineDescriptor, ImportDescriptor, ParticipateXpub, RegisterDescriptor,
+};
 
 use std::path::PathBuf;
 use std::str::FromStr;
-use std::time::Duration;
 
-use async_hwi::DeviceKind;
 use iced::{Command, Element};
-use liana::{
-    config::{BitcoinConfig, BitcoindConfig},
-    descriptors::MultipathDescriptor,
-    miniscript::bitcoin,
-};
+use liana::{config::BitcoindConfig, miniscript::bitcoin};
 
 use crate::ui::component::form;
 
 use crate::installer::{
+    context::Context,
     message::{self, Message},
     view,
 };
@@ -37,34 +34,6 @@ pub trait Step {
     }
 }
 
-#[derive(Clone)]
-pub struct Context {
-    pub bitcoin_config: BitcoinConfig,
-    pub bitcoind_config: Option<BitcoindConfig>,
-    pub descriptor: Option<MultipathDescriptor>,
-    pub hws: Vec<(
-        DeviceKind,
-        bitcoin::util::bip32::Fingerprint,
-        Option<[u8; 32]>,
-    )>,
-    pub data_dir: PathBuf,
-}
-
-impl Context {
-    pub fn new(network: bitcoin::Network, data_dir: PathBuf) -> Self {
-        Self {
-            bitcoin_config: BitcoinConfig {
-                network,
-                poll_interval_secs: Duration::from_secs(30),
-            },
-            hws: Vec::new(),
-            bitcoind_config: None,
-            descriptor: None,
-            data_dir,
-        }
-    }
-}
-
 #[derive(Default)]
 pub struct Welcome {}
 
diff --git a/gui/src/installer/view.rs b/gui/src/installer/view.rs
index a5ef142a..31857563 100644
--- a/gui/src/installer/view.rs
+++ b/gui/src/installer/view.rs
@@ -1,20 +1,23 @@
-use iced::widget::{Button, Checkbox, Column, Container, PickList, Row, Scrollable};
-use iced::{Alignment, Element, Length};
+use iced::widget::{
+    scrollable::Properties, Button, Checkbox, Column, Container, PickList, Row, Scrollable, Space,
+};
+use iced::{alignment, Alignment, Element, Length};
 
 use liana::miniscript::bitcoin;
 
 use crate::{
     hw::HardwareWallet,
     installer::{
+        context::Context,
         message::{self, Message},
-        step::Context,
-        Error,
+        prompt, Error,
     },
     ui::{
         color,
         component::{
-            button, card, collapse, container, form,
+            button, card, collapse, container, form, separation,
             text::{text, Text},
+            tooltip,
         },
         icon,
         util::Collection,
@@ -79,12 +82,12 @@ pub fn welcome<'a>() -> Element<'a, Message> {
                         Button::new(
                             Container::new(
                                 Column::new()
-                                    .width(Length::Units(200))
+                                    .width(Length::Units(250))
                                     .push(icon::wallet_icon().size(50).width(Length::Units(100)))
-                                    .push(text("Create new wallet"))
+                                    .push(text("Create a new wallet"))
                                     .align_items(Alignment::Center),
                             )
-                            .padding(50),
+                            .padding(20),
                         )
                         .style(button::Style::Border.into())
                         .on_press(Message::CreateWallet),
@@ -93,12 +96,26 @@ pub fn welcome<'a>() -> Element<'a, Message> {
                         Button::new(
                             Container::new(
                                 Column::new()
-                                    .width(Length::Units(200))
+                                    .width(Length::Units(250))
+                                    .push(icon::people_icon().size(50).width(Length::Units(100)))
+                                    .push(text("Participate in a new wallet"))
+                                    .align_items(Alignment::Center),
+                            )
+                            .padding(20),
+                        )
+                        .style(button::Style::Border.into())
+                        .on_press(Message::ParticipateWallet),
+                    )
+                    .push(
+                        Button::new(
+                            Container::new(
+                                Column::new()
+                                    .width(Length::Units(250))
                                     .push(icon::import_icon().size(50).width(Length::Units(100)))
-                                    .push(text("Import wallet"))
+                                    .push(text("Import a wallet backup"))
                                     .align_items(Alignment::Center),
                             )
-                            .padding(50),
+                            .padding(20),
                         )
                         .style(button::Style::Border.into())
                         .on_press(Message::ImportWallet),
@@ -106,7 +123,6 @@ pub fn welcome<'a>() -> Element<'a, Message> {
             )
             .width(Length::Fill)
             .height(Length::Fill)
-            .padding(100)
             .spacing(50)
             .align_items(Alignment::Center),
     ))
@@ -117,13 +133,17 @@ pub fn welcome<'a>() -> Element<'a, Message> {
     .into()
 }
 
+#[allow(clippy::too_many_arguments)]
 pub fn define_descriptor<'a>(
     progress: (usize, usize),
     network: bitcoin::Network,
     network_valid: bool,
-    user_xpub: &form::Value<String>,
-    heir_xpub: &form::Value<String>,
+    spending_keys: Vec<Element<'a, Message>>,
+    recovery_keys: Vec<Element<'a, Message>>,
     sequence: &form::Value<String>,
+    spending_threshold: usize,
+    recovery_threshold: usize,
+    valid: bool,
     error: Option<&String>,
 ) -> Element<'a, Message> {
     let row_network = Row::new()
@@ -142,101 +162,182 @@ pub fn define_descriptor<'a>(
             Some(card::warning(
                 "A data directory already exists for this network".to_string(),
             ))
-        });
+        })
+        .padding(50);
 
-    let col_user_xpub = Column::new()
-        .push(text("Your public key:").bold())
+    let col_spending_keys = Column::new()
         .push(
             Row::new()
-                .push(button::border(Some(icon::chip_icon()), "Import").on_press(
-                    Message::DefineDescriptor(message::DefineDescriptor::ImportUserHWXpub),
-                ))
-                .push(
-                    form::Form::new("Xpub", user_xpub, |msg| {
-                        Message::DefineDescriptor(message::DefineDescriptor::UserXpubEdited(msg))
-                    })
-                    .warning(if network == bitcoin::Network::Bitcoin {
-                        "Please enter correct xpub"
+                .spacing(10)
+                .push(Space::with_width(Length::Units(40)))
+                .push(text("Primary path:").bold())
+                .push(tooltip(prompt::DEFINE_DESCRIPTOR_PRIMATRY_PATH_TOOLTIP)),
+        )
+        .push(separation().width(Length::Fill))
+        .push(
+            Container::new(
+                Row::new()
+                    .align_items(Alignment::Center)
+                    .push_maybe(if spending_keys.len() > 1 {
+                        Some(threshsold_input::threshsold_input(
+                            spending_threshold,
+                            spending_keys.len(),
+                            |value| {
+                                Message::DefineDescriptor(
+                                    message::DefineDescriptor::ThresholdEdited(false, value),
+                                )
+                            },
+                        ))
                     } else {
-                        "Please enter correct tpub"
+                        None
                     })
-                    .size(20)
-                    .padding(12),
-                )
-                .push(Container::new(text("/<0;1>/*")))
-                .spacing(5)
-                .align_items(Alignment::Center),
+                    .push(
+                        Scrollable::new(
+                            Row::new()
+                                .spacing(5)
+                                .align_items(Alignment::Center)
+                                .push(Row::with_children(spending_keys).spacing(5))
+                                .push(
+                                    Button::new(
+                                        Container::new(icon::plus_icon().size(50))
+                                            .width(Length::Units(200))
+                                            .height(Length::Units(200))
+                                            .align_y(alignment::Vertical::Center)
+                                            .align_x(alignment::Horizontal::Center),
+                                    )
+                                    .width(Length::Units(200))
+                                    .height(Length::Units(200))
+                                    .style(button::Style::TransparentBorder.into())
+                                    .on_press(
+                                        Message::DefineDescriptor(
+                                            message::DefineDescriptor::AddKey(false),
+                                        ),
+                                    ),
+                                )
+                                .padding(5),
+                        )
+                        .horizontal_scroll(Properties::new().width(3).scroller_width(3)),
+                    ),
+            )
+            .width(Length::Fill)
+            .align_x(alignment::Horizontal::Center),
         )
         .spacing(10);
 
-    let col_heir_xpub = Column::new()
-        .push(text("Public key of the recovery key:").bold())
+    let col_recovery_keys = Column::new()
         .push(
             Row::new()
-                .push(button::border(Some(icon::chip_icon()), "Import").on_press(
-                    Message::DefineDescriptor(message::DefineDescriptor::ImportHeirHWXpub),
-                ))
-                .push(
-                    form::Form::new("Xpub", heir_xpub, |msg| {
-                        Message::DefineDescriptor(message::DefineDescriptor::HeirXpubEdited(msg))
-                    })
-                    .warning(if network == bitcoin::Network::Bitcoin {
-                        "Please enter correct xpub"
-                    } else {
-                        "Please enter correct tpub"
-                    })
-                    .size(20)
-                    .padding(12),
-                )
-                .push(Container::new(text("/<0;1>/*")))
-                .spacing(5)
-                .align_items(Alignment::Center),
+                .push(Space::with_width(Length::Units(50)))
+                .push(text("Recovery path:").bold()),
         )
-        .spacing(10);
-
-    let col_sequence = Column::new()
-        .push(text("Number of block before enabling recovery:").bold())
+        .push(separation().width(Length::Fill))
         .push(
             Container::new(
-                form::Form::new("Number of block", sequence, |msg| {
-                    Message::DefineDescriptor(message::DefineDescriptor::SequenceEdited(msg))
-                })
-                .warning("Please enter correct block number")
-                .size(20)
-                .padding(10),
+                Row::new()
+                    .align_items(Alignment::Center)
+                    .push_maybe(if recovery_keys.len() > 1 {
+                        Some(threshsold_input::threshsold_input(
+                            recovery_threshold,
+                            recovery_keys.len(),
+                            |value| {
+                                Message::DefineDescriptor(
+                                    message::DefineDescriptor::ThresholdEdited(true, value),
+                                )
+                            },
+                        ))
+                    } else {
+                        None
+                    })
+                    .push(
+                        Scrollable::new(
+                            Row::new()
+                                .spacing(5)
+                                .align_items(Alignment::Center)
+                                .push(Row::with_children(recovery_keys).spacing(5))
+                                .push(
+                                    Button::new(
+                                        Container::new(icon::plus_icon().size(50))
+                                            .width(Length::Units(200))
+                                            .height(Length::Units(200))
+                                            .align_y(alignment::Vertical::Center)
+                                            .align_x(alignment::Horizontal::Center),
+                                    )
+                                    .width(Length::Units(200))
+                                    .height(Length::Units(200))
+                                    .style(button::Style::TransparentBorder.into())
+                                    .on_press(
+                                        Message::DefineDescriptor(
+                                            message::DefineDescriptor::AddKey(true),
+                                        ),
+                                    ),
+                                )
+                                .padding(5),
+                        )
+                        .horizontal_scroll(Properties::new().width(3).scroller_width(3)),
+                    ),
             )
-            .width(Length::Units(150)),
+            .width(Length::Fill)
+            .align_x(alignment::Horizontal::Center),
         )
         .spacing(10);
 
+    let col_sequence = Container::new(
+        Row::new()
+            .spacing(50)
+            .align_items(Alignment::Center)
+            .push(Container::new(icon::arrow_down().size(50)).align_x(alignment::Horizontal::Right))
+            .push(
+                Column::new()
+                    .push(
+                        Row::new()
+                            .spacing(10)
+                            .push(text("Blocks before recovery:").bold())
+                            .push(tooltip(prompt::DEFINE_DESCRIPTOR_SEQUENCE_TOOLTIP)),
+                    )
+                    .push(
+                        Container::new(
+                            form::Form::new("Number of blocks", sequence, |msg| {
+                                Message::DefineDescriptor(
+                                    message::DefineDescriptor::SequenceEdited(msg),
+                                )
+                            })
+                            .warning("Please enter correct block number")
+                            .size(20)
+                            .padding(10),
+                        )
+                        .width(Length::Units(150)),
+                    )
+                    .spacing(10),
+            )
+            .padding(20),
+    )
+    .width(Length::Fill)
+    .align_x(alignment::Horizontal::Center);
+
     layout(
         progress,
         Column::new()
+            .push(Space::with_height(Length::Units(30)))
             .push(text("Create the wallet").bold().size(50))
             .push(
                 Column::new()
                     .push(row_network)
-                    .push(col_user_xpub)
+                    .push(col_spending_keys)
                     .push(col_sequence)
-                    .push(col_heir_xpub)
+                    .push(col_recovery_keys)
                     .spacing(25),
             )
-            .push(
-                if user_xpub.value.is_empty()
-                    && heir_xpub.value.is_empty()
-                    && sequence.value.is_empty()
-                {
-                    button::primary(None, "Next").width(Length::Units(200))
-                } else {
-                    button::primary(None, "Next")
-                        .width(Length::Units(200))
-                        .on_press(Message::Next)
-                },
-            )
+            .push(if !valid {
+                button::primary(None, "Next").width(Length::Units(200))
+            } else {
+                button::primary(None, "Next")
+                    .width(Length::Units(200))
+                    .on_press(Message::Next)
+            })
             .push_maybe(error.map(|e| card::error("Failed to create descriptor", e.to_string())))
+            .push(Space::with_height(Length::Units(20)))
             .width(Length::Fill)
             .height(Length::Fill)
-            .padding(100)
             .spacing(50)
             .align_items(Alignment::Center),
     )
@@ -244,6 +345,7 @@ pub fn define_descriptor<'a>(
 
 pub fn import_descriptor<'a>(
     progress: (usize, usize),
+    change_network: bool,
     network: bitcoin::Network,
     network_valid: bool,
     imported_descriptor: &form::Value<String>,
@@ -284,7 +386,11 @@ pub fn import_descriptor<'a>(
             .push(
                 Column::new()
                     .spacing(20)
-                    .push(row_network)
+                    .push_maybe(if change_network {
+                        Some(row_network)
+                    } else {
+                        None
+                    })
                     .push(col_descriptor),
             )
             .push(if imported_descriptor.value.is_empty() {
@@ -303,6 +409,170 @@ pub fn import_descriptor<'a>(
     )
 }
 
+pub fn hardware_wallet_xpubs<'a>(
+    i: usize,
+    xpubs: &'a Vec<String>,
+    hw: &HardwareWallet,
+    processing: bool,
+    error: Option<&Error>,
+) -> Element<'a, Message> {
+    let mut bttn = Button::new(
+        Row::new()
+            .align_items(Alignment::Center)
+            .push(
+                Column::new()
+                    .push(text(format!("{}", hw.kind)).bold())
+                    .push(text(format!("fingerprint: {}", hw.fingerprint)).small())
+                    .spacing(5)
+                    .width(Length::Fill),
+            )
+            .push_maybe(error.map(|e| {
+                iced::widget::tooltip(
+                    Row::new()
+                        .spacing(5)
+                        .align_items(Alignment::Center)
+                        .push(icon::warning_icon().style(color::ALERT))
+                        .push(text("An error occured").style(color::ALERT)),
+                    e,
+                    iced::widget::tooltip::Position::Bottom,
+                )
+                .style(card::ErrorCardStyle)
+            })),
+    )
+    .padding(10)
+    .style(button::Style::TransparentBorder.into())
+    .width(Length::Fill);
+    if !processing {
+        bttn = bttn.on_press(Message::Select(i));
+    }
+    Container::new(
+        Column::new()
+            .push(bttn)
+            .push_maybe(if xpubs.is_empty() {
+                None
+            } else {
+                Some(separation().width(Length::Fill))
+            })
+            .push_maybe(if xpubs.is_empty() {
+                None
+            } else {
+                Some(xpubs.iter().fold(Column::new().padding(15), |col, xpub| {
+                    col.push(
+                        Row::new()
+                            .spacing(5)
+                            .align_items(Alignment::Center)
+                            .push(
+                                Container::new(
+                                    Scrollable::new(Container::new(text(xpub).small()).padding(10))
+                                        .horizontal_scroll(
+                                            Properties::new().width(2).scroller_width(2),
+                                        ),
+                                )
+                                .width(Length::Fill),
+                            )
+                            .push(
+                                Container::new(
+                                    button::border(Some(icon::clipboard_icon()), "Copy")
+                                        .on_press(Message::Clibpboard(xpub.clone()))
+                                        .width(Length::Shrink),
+                                )
+                                .padding(10),
+                            ),
+                    )
+                }))
+            })
+            .push_maybe(if !xpubs.is_empty() {
+                Some(
+                    Container::new(if !processing {
+                        button::border(Some(icon::plus_icon()), "New public key")
+                            .on_press(Message::Select(i))
+                    } else {
+                        button::border(Some(icon::plus_icon()), "New public key")
+                    })
+                    .padding(10),
+                )
+            } else {
+                None
+            }),
+    )
+    .style(card::SimpleCardStyle)
+    .into()
+}
+
+pub fn participate_xpub(
+    progress: (usize, usize),
+    network: bitcoin::Network,
+    network_valid: bool,
+    hws: Vec<Element<Message>>,
+    shared: bool,
+) -> Element<Message> {
+    let row_network = Row::new()
+        .spacing(10)
+        .align_items(Alignment::Center)
+        .push(text("Network:").bold())
+        .push(Container::new(
+            PickList::new(&NETWORKS[..], Some(Network::from(network)), |net| {
+                Message::Network(net.into())
+            })
+            .padding(10),
+        ))
+        .push_maybe(if network_valid {
+            None
+        } else {
+            Some(card::warning(
+                "A data directory already exists for this network".to_string(),
+            ))
+        });
+
+    layout(
+        progress,
+        Column::new()
+            .push(text("Share your public keys").bold().size(50))
+            .push(
+                Column::new()
+                    .spacing(20)
+                    .width(Length::Fill)
+                    .push(row_network),
+            )
+            .push(
+                Column::new()
+                    .push(
+                        Row::new()
+                            .spacing(10)
+                            .align_items(Alignment::Center)
+                            .push(
+                                Container::new(text("Generate an extended public key by selecting a signing device:").bold())
+                                    .width(Length::Fill),
+                            )
+                            .push(
+                                button::border(Some(icon::reload_icon()), "Refresh")
+                                    .on_press(Message::Reload),
+                            ),
+                    )
+                    .spacing(10)
+                    .push(Column::with_children(hws).spacing(10))
+                    .width(Length::Fill),
+            )
+            .push(Checkbox::new(
+                "I have shared my public keys",
+                shared,
+                Message::UserActionDone,
+            ))
+            .push(if shared {
+                button::primary(None, "Next")
+                    .width(Length::Units(200))
+                    .on_press(Message::Next)
+            } else {
+                button::primary(None, "Next").width(Length::Units(200))
+            })
+            .width(Length::Fill)
+            .height(Length::Fill)
+            .padding(100)
+            .spacing(50)
+            .align_items(Alignment::Center),
+    )
+}
+
 pub fn register_descriptor<'a>(
     progress: (usize, usize),
     descriptor: String,
@@ -393,7 +663,7 @@ pub fn backup_descriptor<'a>(
             )
             .push(
                 Column::new()
-                    .push(text(super::prompt::BACKUP_DESCRIPTOR_MESSAGE))
+                    .push(text(prompt::BACKUP_DESCRIPTOR_MESSAGE))
                     .push(collapse::Collapse::new(
                         || {
                             Button::new(
@@ -435,7 +705,7 @@ pub fn backup_descriptor<'a>(
             .push(Checkbox::new(
                 "I have backed up my descriptor",
                 done,
-                Message::BackupDone,
+                Message::UserActionDone,
             ))
             .push(if done {
                 button::primary(None, "Next")
@@ -453,7 +723,7 @@ pub fn backup_descriptor<'a>(
 }
 
 pub fn help_backup<'a>() -> Element<'a, Message> {
-    text(super::prompt::BACKUP_DESCRIPTOR_HELP).small().into()
+    text(prompt::BACKUP_DESCRIPTOR_HELP).small().into()
 }
 
 pub fn define_bitcoin<'a>(
@@ -627,65 +897,294 @@ pub fn install<'a>(
     layout(progress, col)
 }
 
-pub fn hardware_wallet_xpubs_modal<'a>(
-    is_heir: bool,
-    hws: &[HardwareWallet],
-    error: Option<&Error>,
-    processing: bool,
-    chosen_hw: Option<usize>,
-) -> Element<'a, Message> {
-    modal(
+pub fn undefined_descriptor_key<'a>() -> Element<'a, message::DefineKey> {
+    card::simple(
         Column::new()
+            .width(Length::Fill)
+            .align_items(Alignment::Center)
             .push(
-                text(if is_heir {
-                    "Import the recovery public key"
-                } else {
-                    "Import the user public key"
-                })
-                .bold()
-                .size(50),
+                Row::new()
+                    .align_items(Alignment::Center)
+                    .push(Space::with_width(Length::Fill))
+                    .push(
+                        Button::new(icon::cross_icon())
+                            .style(button::Style::Transparent.into())
+                            .on_press(message::DefineKey::Delete),
+                    ),
             )
-            .push_maybe(error.map(|e| card::error("Failed to import xpub", e.to_string())))
             .push(
-                Column::new()
-                    .push(
-                        Row::new()
-                            .spacing(10)
+                Container::new(
+                    Column::new()
+                        .spacing(15)
+                        .align_items(Alignment::Center)
+                        .push(
+                            Scrollable::new(
+                                icon::key_icon()
+                                    .style(color::DARK_GREY)
+                                    .size(50)
+                                    .width(Length::Units(50)),
+                            )
+                            .horizontal_scroll(Properties::new().width(2).scroller_width(2)),
+                        )
+                        .push(icon::circle_check_icon().style(color::FOREGROUND).size(50)),
+                )
+                .height(Length::Fill)
+                .align_y(alignment::Vertical::Center),
+            )
+            .push(
+                button::border(Some(icon::pencil_icon()), "Set").on_press(message::DefineKey::Edit),
+            )
+            .push(Space::with_height(Length::Units(5))),
+    )
+    .padding(5)
+    .height(Length::Units(200))
+    .width(Length::Units(200))
+    .into()
+}
+
+pub fn defined_descriptor_key(
+    name: &str,
+    valid: bool,
+    duplicate_key: bool,
+    duplicate_name: bool,
+) -> Element<message::DefineKey> {
+    let col = Column::new()
+        .width(Length::Fill)
+        .align_items(Alignment::Center)
+        .push(
+            Row::new()
+                .align_items(Alignment::Center)
+                .push(Space::with_width(Length::Fill))
+                .push(
+                    Button::new(icon::cross_icon())
+                        .style(button::Style::Transparent.into())
+                        .on_press(message::DefineKey::Delete),
+                ),
+        )
+        .push(
+            Column::new()
+                .align_items(Alignment::Center)
+                .spacing(5)
+                .push(
+                    Container::new(
+                        Column::new()
+                            .spacing(15)
                             .align_items(Alignment::Center)
                             .push(
-                                Container::new(
-                                    text(format!("{} hardware wallets connected", hws.len()))
-                                        .bold(),
-                                )
-                                .width(Length::Fill),
+                                Scrollable::new(text(name).bold()).horizontal_scroll(
+                                    Properties::new().width(2).scroller_width(2),
+                                ),
                             )
                             .push(
-                                button::border(Some(icon::reload_icon()), "Refresh")
-                                    .on_press(Message::Reload),
+                                icon::circle_check_icon()
+                                    .style(color::SUCCESS)
+                                    .size(40)
+                                    .width(Length::Units(50)),
                             ),
                     )
-                    .spacing(10)
-                    .push(
-                        hws.iter()
-                            .enumerate()
-                            .fold(Column::new().spacing(10), |col, (i, hw)| {
+                    .height(Length::Fill)
+                    .align_y(alignment::Vertical::Center),
+                )
+                .height(Length::Fill),
+        )
+        .push(button::border(Some(icon::pencil_icon()), "Edit").on_press(message::DefineKey::Edit))
+        .push(Space::with_height(Length::Units(5)));
+
+    if !valid {
+        Column::new()
+            .align_items(Alignment::Center)
+            .push(
+                card::invalid(col)
+                    .padding(5)
+                    .height(Length::Units(200))
+                    .width(Length::Units(200)),
+            )
+            .push(
+                text("Key is for a different network")
+                    .small()
+                    .style(color::ALERT),
+            )
+            .into()
+    } else if duplicate_key {
+        Column::new()
+            .align_items(Alignment::Center)
+            .push(
+                card::invalid(col)
+                    .padding(5)
+                    .height(Length::Units(200))
+                    .width(Length::Units(200)),
+            )
+            .push(text("Duplicate key").small().style(color::ALERT))
+            .into()
+    } else if duplicate_name {
+        Column::new()
+            .align_items(Alignment::Center)
+            .push(
+                card::invalid(col)
+                    .padding(5)
+                    .height(Length::Units(200))
+                    .width(Length::Units(200)),
+            )
+            .push(text("Duplicate name").small().style(color::ALERT))
+            .into()
+    } else {
+        card::simple(col)
+            .padding(5)
+            .height(Length::Units(200))
+            .width(Length::Units(200))
+            .into()
+    }
+}
+
+#[allow(clippy::too_many_arguments)]
+pub fn edit_key_modal<'a>(
+    network: bitcoin::Network,
+    hws: &[HardwareWallet],
+    error: Option<&Error>,
+    processing: bool,
+    chosen_hw: Option<usize>,
+    form_xpub: &form::Value<String>,
+    form_name: &'a form::Value<String>,
+    edit_name: bool,
+) -> Element<'a, Message> {
+    Column::new()
+        .push_maybe(error.map(|e| card::error("Failed to import xpub", e.to_string())))
+        .push(card::simple(
+            Column::new()
+                .spacing(25)
+                .push(if !hws.is_empty() {
+                    Column::new()
+                        .push(
+                            Row::new()
+                                .spacing(10)
+                                .align_items(Alignment::Center)
+                                .push(
+                                    Container::new(text("Select a hardware wallet:").bold())
+                                        .width(Length::Fill),
+                                )
+                                .push(
+                                    button::border(Some(icon::reload_icon()), "Refresh")
+                                        .on_press(Message::Reload),
+                                ),
+                        )
+                        .spacing(10)
+                        .push(hws.iter().enumerate().fold(
+                            Column::new().spacing(10),
+                            |col, (i, hw)| {
                                 col.push(hw_list_view(
                                     i,
                                     hw,
                                     Some(i) == chosen_hw,
                                     processing,
-                                    false,
+                                    !processing
+                                        && Some(i) == chosen_hw
+                                        && form_xpub.valid
+                                        && !form_xpub.value.is_empty(),
                                 ))
-                            }),
-                    )
-                    .width(Length::Fill),
-            )
-            .width(Length::Fill)
-            .height(Length::Fill)
-            .padding(100)
-            .spacing(50)
-            .align_items(Alignment::Center),
-    )
+                            },
+                        ))
+                        .width(Length::Fill)
+                } else {
+                    Column::new()
+                        .push(
+                            Row::new()
+                                .spacing(15)
+                                .width(Length::Fill)
+                                .push(
+                                    text("Or connect a hardware wallet")
+                                        .bold()
+                                        .width(Length::Fill),
+                                )
+                                .push(button::border(None, "Refresh").on_press(Message::Reload))
+                                .align_items(Alignment::Center),
+                        )
+                        .width(Length::Fill)
+                })
+                .push(
+                    Column::new()
+                        .spacing(5)
+                        .push(text("Or enter an extended public key:").bold())
+                        .push(
+                            Row::new()
+                                .push(
+                                    form::Form::new("Extended public key", form_xpub, |msg| {
+                                        Message::DefineDescriptor(
+                                            message::DefineDescriptor::XPubEdited(msg),
+                                        )
+                                    })
+                                    .warning(if network == bitcoin::Network::Bitcoin {
+                                        "Please enter correct xpub with origin"
+                                    } else {
+                                        "Please enter correct tpub with origin"
+                                    })
+                                    .size(20)
+                                    .padding(10),
+                                )
+                                .spacing(10)
+                                .push(Container::new(text("/<0;1>/*")).padding(5)),
+                        ),
+                )
+                .push(
+                    if !edit_name && !form_xpub.value.is_empty() && form_xpub.valid {
+                        Column::new().push(
+                            Row::new()
+                                .push(
+                                    Column::new()
+                                        .spacing(5)
+                                        .width(Length::Fill)
+                                        .push(
+                                            Row::new()
+                                                .spacing(5)
+                                                .push(text("Fingerprint alias:").bold())
+                                                .push(tooltip(
+                                                    prompt::DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP,
+                                                )),
+                                        )
+                                        .push(text(&form_name.value)),
+                                )
+                                .push(button::border(Some(icon::pencil_icon()), "Edit").on_press(
+                                    Message::DefineDescriptor(message::DefineDescriptor::EditName),
+                                )),
+                        )
+                    } else if !form_xpub.value.is_empty() && form_xpub.valid {
+                        Column::new()
+                            .spacing(5)
+                            .push(
+                                Row::new()
+                                    .spacing(5)
+                                    .push(text("Fingerprint alias:").bold())
+                                    .push(tooltip(prompt::DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP)),
+                            )
+                            .push(
+                                form::Form::new("Alias", form_name, |msg| {
+                                    Message::DefineDescriptor(
+                                        message::DefineDescriptor::NameEdited(msg),
+                                    )
+                                })
+                                .warning("Please enter correct alias")
+                                .size(20)
+                                .padding(10),
+                            )
+                    } else {
+                        Column::new()
+                    },
+                )
+                .push(
+                    if form_xpub.valid && !form_xpub.value.is_empty() && !form_name.value.is_empty()
+                    {
+                        button::primary(None, "Apply")
+                            .on_press(Message::DefineDescriptor(
+                                message::DefineDescriptor::ConfirmXpub,
+                            ))
+                            .width(Length::Units(200))
+                    } else {
+                        button::primary(None, "Apply").width(Length::Units(100))
+                    },
+                )
+                .align_items(Alignment::Center),
+        ))
+        .width(Length::Units(600))
+        .into()
 }
 
 fn hw_list_view<'a>(
@@ -757,22 +1256,103 @@ fn layout<'a>(
     .into()
 }
 
-fn modal<'a>(content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
-    Container::new(Scrollable::new(
-        Column::new()
-            .push(
-                Row::new().push(Column::new().width(Length::Fill)).push(
-                    Container::new(
-                        button::primary(Some(icon::cross_icon()), "Close").on_press(Message::Close),
-                    )
-                    .padding(10),
-                ),
-            )
-            .push(Container::new(content).width(Length::Fill).center_x()),
-    ))
-    .center_x()
-    .height(Length::Fill)
-    .width(Length::Fill)
-    .style(container::Style::Background)
-    .into()
+mod threshsold_input {
+    use crate::ui::{
+        component::{button, text::*},
+        icon,
+    };
+    use iced::alignment::{self, Alignment};
+    use iced::widget::{Button, Column, Container};
+    use iced::{Element, Length};
+    use iced_lazy::{self, Component};
+
+    pub struct ThresholdInput<Message> {
+        value: usize,
+        max: usize,
+        on_change: Box<dyn Fn(usize) -> Message>,
+    }
+
+    pub fn threshsold_input<Message>(
+        value: usize,
+        max: usize,
+        on_change: impl Fn(usize) -> Message + 'static,
+    ) -> ThresholdInput<Message> {
+        ThresholdInput::new(value, max, on_change)
+    }
+
+    #[derive(Debug, Clone)]
+    pub enum Event {
+        IncrementPressed,
+        DecrementPressed,
+    }
+
+    impl<Message> ThresholdInput<Message> {
+        pub fn new(
+            value: usize,
+            max: usize,
+            on_change: impl Fn(usize) -> Message + 'static,
+        ) -> Self {
+            Self {
+                value,
+                max,
+                on_change: Box::new(on_change),
+            }
+        }
+    }
+
+    impl<Message> Component<Message, iced::Renderer> for ThresholdInput<Message> {
+        type State = ();
+        type Event = Event;
+
+        fn update(&mut self, _state: &mut Self::State, event: Event) -> Option<Message> {
+            match event {
+                Event::IncrementPressed => {
+                    if self.value < self.max {
+                        Some((self.on_change)(self.value.saturating_add(1)))
+                    } else {
+                        None
+                    }
+                }
+                Event::DecrementPressed => {
+                    if self.value > 1 {
+                        Some((self.on_change)(self.value.saturating_sub(1)))
+                    } else {
+                        None
+                    }
+                }
+            }
+        }
+
+        fn view(&self, _state: &Self::State) -> Element<Self::Event> {
+            let button = |label, on_press| {
+                Button::new(label)
+                    .style(button::Style::Transparent.into())
+                    .width(Length::Units(50))
+                    .on_press(on_press)
+            };
+
+            Column::new()
+                .height(Length::Units(200))
+                .width(Length::Units(100))
+                .push(button(icon::up_icon().size(40), Event::IncrementPressed))
+                .push(text("Threshold:").small().bold())
+                .push(
+                    Container::new(text(format!("{}/{}", self.value, self.max)).size(50))
+                        .height(Length::Fill)
+                        .align_y(alignment::Vertical::Center),
+                )
+                .push(button(icon::down_icon().size(40), Event::DecrementPressed))
+                .align_items(Alignment::Center)
+                .into()
+        }
+    }
+
+    impl<'a, Message> From<ThresholdInput<Message>> for Element<'a, Message>
+    where
+        Message: 'a,
+    {
+        fn from(numeric_input: ThresholdInput<Message>) -> Self {
+            iced_lazy::component(numeric_input)
+        }
+    }
 }
diff --git a/gui/src/loader.rs b/gui/src/loader.rs
index 558d647a..477bf8e7 100644
--- a/gui/src/loader.rs
+++ b/gui/src/loader.rs
@@ -17,7 +17,12 @@ use liana::{
 };
 
 use crate::{
-    app::config::{default_datadir, Config as GUIConfig},
+    app::{
+        cache::Cache,
+        config::{default_datadir, Config as GUIConfig},
+        settings::{self, Settings},
+        wallet::Wallet,
+    },
     daemon::{client, embedded::EmbeddedDaemon, model::*, Daemon, DaemonError},
     ui::{
         component::{button, notification, text::*},
@@ -30,6 +35,7 @@ type Lianad = client::Lianad<client::jsonrpc::JsonRPCClient>;
 
 pub struct Loader {
     pub datadir_path: Option<PathBuf>,
+    pub network: bitcoin::Network,
     pub gui_config: GUIConfig,
     pub daemon_started: bool,
 
@@ -47,16 +53,12 @@ pub enum Step {
     Error(Box<Error>),
 }
 
+#[allow(clippy::type_complexity)]
 #[derive(Debug)]
 pub enum Message {
     View(ViewMessage),
     Syncing(Result<GetInfoResult, DaemonError>),
-    Synced(
-        GetInfoResult,
-        Vec<Coin>,
-        Vec<SpendTx>,
-        Arc<dyn Daemon + Sync + Send>,
-    ),
+    Synced(Result<(Arc<Wallet>, Cache, Arc<dyn Daemon + Sync + Send>), Error>),
     Started(Result<Arc<dyn Daemon + Sync + Send>, Error>),
     Loaded(Result<Arc<dyn Daemon + Sync + Send>, Error>),
     Failure(DaemonError),
@@ -75,6 +77,7 @@ impl Loader {
         .unwrap();
         (
             Loader {
+                network: daemon_config.bitcoin_config.network,
                 datadir_path,
                 daemon_config: daemon_config.clone(),
                 gui_config,
@@ -141,18 +144,47 @@ impl Loader {
                     Ok(info) => {
                         if (info.sync - 1.0_f64).abs() < f64::EPSILON {
                             let daemon = daemon.clone();
+                            let settings_path =
+                                settings_path(&self.datadir_path, self.network).unwrap();
+                            let gui_config_hws = self
+                                .gui_config
+                                .hardware_wallets
+                                .as_ref()
+                                .cloned()
+                                .unwrap_or_default();
                             return Command::perform(
                                 async move {
-                                    let coins = daemon
-                                        .list_coins()
-                                        .map(|res| res.coins)
-                                        .unwrap_or_else(|_| Vec::new());
-                                    let spend_txs = daemon
-                                        .list_spend_transactions()
-                                        .unwrap_or_else(|_| Vec::new());
-                                    (info, coins, spend_txs, daemon)
+                                    let coins = daemon.list_coins().map(|res| res.coins)?;
+                                    let spend_txs = daemon.list_spend_transactions()?;
+                                    let cache = Cache {
+                                        network: info.network,
+                                        blockheight: info.block_height,
+                                        coins,
+                                        spend_txs,
+                                        ..Default::default()
+                                    };
+                                    let wallet = match Settings::from_file(&settings_path) {
+                                        Ok(settings) => {
+                                            if let Some(wallet_setting) = settings.wallets.first() {
+                                                Wallet::legacy(info.descriptors.main)
+                                                    .with_harware_wallets(
+                                                        wallet_setting.hardware_wallets.clone(),
+                                                    )
+                                                    .with_key_aliases(wallet_setting.keys_aliases())
+                                            } else {
+                                                Wallet::legacy(info.descriptors.main)
+                                                    .with_harware_wallets(gui_config_hws)
+                                            }
+                                        }
+                                        Err(settings::SettingsError::NotFound) => {
+                                            Wallet::legacy(info.descriptors.main)
+                                                .with_harware_wallets(gui_config_hws)
+                                        }
+                                        Err(e) => return Err(e.into()),
+                                    };
+                                    Ok((Arc::new(wallet), cache, daemon))
                                 },
-                                |res| Message::Synced(res.0, res.1, res.2, res.3),
+                                Message::Synced,
                             );
                         } else {
                             *progress = info.sync
@@ -333,6 +365,7 @@ async fn sync(
 #[allow(clippy::large_enum_variant)]
 #[derive(Debug)]
 pub enum Error {
+    Settings(settings::SettingsError),
     Config(ConfigError),
     Daemon(DaemonError),
 }
@@ -340,12 +373,19 @@ pub enum Error {
 impl std::fmt::Display for Error {
     fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
         match self {
+            Self::Settings(e) => write!(f, "Settings error: {}", e),
             Self::Config(e) => write!(f, "Config error: {}", e),
             Self::Daemon(e) => write!(f, "Liana daemon error: {}", e),
         }
     }
 }
 
+impl From<settings::SettingsError> for Error {
+    fn from(error: settings::SettingsError) -> Self {
+        Error::Settings(error)
+    }
+}
+
 impl From<ConfigError> for Error {
     fn from(error: ConfigError) -> Self {
         Error::Config(error)
@@ -372,3 +412,18 @@ fn socket_path(
     path.push("lianad_rpc");
     Ok(path)
 }
+
+/// default liana settings path is .liana/bitcoin/settings.json
+fn settings_path(
+    datadir: &Option<PathBuf>,
+    network: bitcoin::Network,
+) -> Result<PathBuf, ConfigError> {
+    let mut path = if let Some(ref datadir) = datadir {
+        datadir.clone()
+    } else {
+        default_datadir().map_err(|_| ConfigError::DatadirNotFound)?
+    };
+    path.push(network.to_string());
+    path.push(settings::DEFAULT_FILE_NAME);
+    Ok(path)
+}
diff --git a/gui/src/main.rs b/gui/src/main.rs
index 917aaf1b..397891b3 100644
--- a/gui/src/main.rs
+++ b/gui/src/main.rs
@@ -11,9 +11,7 @@ use liana::{config::Config as DaemonConfig, miniscript::bitcoin};
 use liana_gui::{
     app::{
         self,
-        cache::Cache,
         config::{default_datadir, ConfigError},
-        wallet::Wallet,
         App,
     },
     installer::{self, Installer},
@@ -205,17 +203,7 @@ impl Application for GUI {
                     )));
                     Command::none()
                 }
-                loader::Message::Synced(info, coins, spend_txs, daemon) => {
-                    let cache = Cache {
-                        network: info.network,
-                        blockheight: info.block_height,
-                        coins,
-                        spend_txs,
-                        ..Default::default()
-                    };
-
-                    let wallet = Wallet::new(info.descriptors.main);
-
+                loader::Message::Synced(Ok((wallet, cache, daemon))) => {
                     let (app, command) = App::new(cache, wallet, loader.gui_config.clone(), daemon);
                     self.state = State::App(app);
                     command.map(|msg| Message::Run(Box::new(msg)))
diff --git a/gui/src/ui/component/card.rs b/gui/src/ui/component/card.rs
index 52e8b92d..025b6768 100644
--- a/gui/src/ui/component/card.rs
+++ b/gui/src/ui/component/card.rs
@@ -35,6 +35,36 @@ impl From<SimpleCardStyle> for iced::theme::Container {
     }
 }
 
+pub fn invalid<'a, T: 'a, C: Into<Element<'a, T>>>(content: C) -> widget::Container<'a, T> {
+    Container::new(content).padding(15).style(InvalidCardStyle)
+}
+
+pub struct InvalidCardStyle;
+impl widget::container::StyleSheet for InvalidCardStyle {
+    type Style = iced::Theme;
+    fn appearance(&self, _style: &Self::Style) -> widget::container::Appearance {
+        widget::container::Appearance {
+            border_radius: 10.0,
+            border_color: color::ALERT,
+            border_width: 1.0,
+            background: color::FOREGROUND.into(),
+            ..widget::container::Appearance::default()
+        }
+    }
+}
+
+impl From<InvalidCardStyle> for Box<dyn widget::container::StyleSheet<Style = iced::Theme>> {
+    fn from(s: InvalidCardStyle) -> Box<dyn widget::container::StyleSheet<Style = iced::Theme>> {
+        Box::new(s)
+    }
+}
+
+impl From<InvalidCardStyle> for iced::theme::Container {
+    fn from(i: InvalidCardStyle) -> iced::theme::Container {
+        iced::theme::Container::Custom(i.into())
+    }
+}
+
 /// display an error card with the message and the error in a tooltip.
 pub fn warning<'a, T: 'a>(message: String) -> widget::Container<'a, T> {
     Container::new(
diff --git a/gui/src/ui/component/mod.rs b/gui/src/ui/component/mod.rs
index 3f97ac8d..e9346029 100644
--- a/gui/src/ui/component/mod.rs
+++ b/gui/src/ui/component/mod.rs
@@ -7,6 +7,9 @@ pub mod form;
 pub mod modal;
 pub mod notification;
 pub mod text;
+pub mod tooltip;
+
+pub use tooltip::tooltip;
 
 use iced::widget::{Column, Container, Text};
 use iced::Length;
diff --git a/gui/src/ui/component/tooltip.rs b/gui/src/ui/component/tooltip.rs
new file mode 100644
index 00000000..bb254e9d
--- /dev/null
+++ b/gui/src/ui/component/tooltip.rs
@@ -0,0 +1,36 @@
+use crate::ui::{color, icon};
+use iced::widget::{self, Tooltip};
+
+pub fn tooltip<'a, T: 'a>(help: &'static str) -> Tooltip<'a, T> {
+    Tooltip::new(
+        icon::tooltip_icon().style(color::DARK_GREY),
+        help,
+        widget::tooltip::Position::Right,
+    )
+    .style(TooltipStyle)
+}
+pub struct TooltipStyle;
+impl widget::container::StyleSheet for TooltipStyle {
+    type Style = iced::Theme;
+    fn appearance(&self, _style: &Self::Style) -> widget::container::Appearance {
+        widget::container::Appearance {
+            border_radius: 10.0,
+            border_color: color::DARK_GREY,
+            border_width: 1.5,
+            background: color::FOREGROUND.into(),
+            ..widget::container::Appearance::default()
+        }
+    }
+}
+
+impl From<TooltipStyle> for Box<dyn widget::container::StyleSheet<Style = iced::Theme>> {
+    fn from(s: TooltipStyle) -> Box<dyn widget::container::StyleSheet<Style = iced::Theme>> {
+        Box::new(s)
+    }
+}
+
+impl From<TooltipStyle> for iced::theme::Container {
+    fn from(i: TooltipStyle) -> iced::theme::Container {
+        iced::theme::Container::Custom(i.into())
+    }
+}
diff --git a/gui/src/ui/icon.rs b/gui/src/ui/icon.rs
index fc7d7c5f..ec082572 100644
--- a/gui/src/ui/icon.rs
+++ b/gui/src/ui/icon.rs
@@ -13,6 +13,10 @@ fn icon(unicode: char) -> Text<'static> {
         .size(20)
 }
 
+pub fn arrow_down() -> Text<'static> {
+    icon('\u{F128}')
+}
+
 pub fn recovery_icon() -> Text<'static> {
     icon('\u{F467}')
 }
@@ -117,6 +121,10 @@ pub fn circle_check_icon() -> Text<'static> {
     icon('\u{F26B}')
 }
 
+pub fn circle_cross_icon() -> Text<'static> {
+    icon('\u{F623}')
+}
+
 pub fn network_icon() -> Text<'static> {
     icon('\u{F40D}')
 }
@@ -206,3 +214,15 @@ pub fn collapse_icon() -> Text<'static> {
 pub fn collapsed_icon() -> Text<'static> {
     icon('\u{F282}')
 }
+
+pub fn down_icon() -> Text<'static> {
+    icon('\u{F279}')
+}
+
+pub fn up_icon() -> Text<'static> {
+    icon('\u{F27C}')
+}
+
+pub fn people_icon() -> Text<'static> {
+    icon('\u{F4CF}')
+}
diff --git a/src/config.rs b/src/config.rs
index 335401f0..dc6b831b 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -227,7 +227,7 @@ mod tests {
             data_dir = "/home/wizardsardine/custom/folder/"
             daemon = false
             log_level = "debug"
-            main_descriptor = "wsh(andor(pk([aabbccdd]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk([aabbccdd]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#dw4ulnrs"
+            main_descriptor = "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#5f6qd0d9"
 
             [bitcoin_config]
             network = "bitcoin"
@@ -244,7 +244,7 @@ mod tests {
             data_dir = '/home/wizardsardine/custom/folder/'
             daemon = false
             log_level = 'TRACE'
-            main_descriptor = 'wsh(andor(pk([aabbccdd]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk([aabbccdd]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#dw4ulnrs'
+            main_descriptor = 'wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#5f6qd0d9'
 
             [bitcoin_config]
             network = 'bitcoin'
@@ -265,7 +265,7 @@ mod tests {
             log_level = "trace"
             data_dir = "/home/wizardsardine/custom/folder/"
 
-            main_descriptor = "wsh(andor(pk([aabbccdd]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk([aabbccdd]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#y5wcna2e"
+            main_descriptor = "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#y5wcna2e"
 
             [bitcoin_config]
             network = "bitcoin"
diff --git a/src/database/sqlite/mod.rs b/src/database/sqlite/mod.rs
index d12064b8..e9a3e88a 100644
--- a/src/database/sqlite/mod.rs
+++ b/src/database/sqlite/mod.rs
@@ -581,7 +581,7 @@ mod tests {
     use bitcoin::{hashes::Hash, util::bip32};
 
     fn dummy_options() -> FreshDbOptions {
-        let desc_str = "wsh(andor(pk([aabbccdd]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk([aabbccdd]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#dw4ulnrs";
+        let desc_str = "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#5f6qd0d9";
         let main_descriptor = MultipathDescriptor::from_str(desc_str).unwrap();
         FreshDbOptions {
             bitcoind_network: bitcoin::Network::Bitcoin,
@@ -630,7 +630,7 @@ mod tests {
             .to_string()
             .contains("Database was created for network");
         fs::remove_file(&db_path).unwrap();
-        let other_desc_str = "wsh(andor(pk([aabbccdd]tpubDExU4YLJkyQ9RRbVScQq2brFxWWha7WmAUByPWyaWYwmcTv3Shx8aHp6mVwuE5n4TeM4z5DTWGf2YhNPmXtfvyr8cUDVvA3txdrFnFgNdF7/<0;1>/*),older(10000),pk([aabbccdd]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))";
+        let other_desc_str = "wsh(andor(pk(tpubDExU4YLJkyQ9RRbVScQq2brFxWWha7WmAUByPWyaWYwmcTv3Shx8aHp6mVwuE5n4TeM4z5DTWGf2YhNPmXtfvyr8cUDVvA3txdrFnFgNdF7/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))";
         let other_desc = MultipathDescriptor::from_str(other_desc_str).unwrap();
         let db = SqliteDb::new(db_path.clone(), Some(options.clone()), &secp).unwrap();
         db.sanity_check(bitcoin::Network::Bitcoin, &other_desc)
diff --git a/src/descriptors.rs b/src/descriptors.rs
index fa038c0d..420c58e6 100644
--- a/src/descriptors.rs
+++ b/src/descriptors.rs
@@ -208,7 +208,6 @@ fn csv_check(csv_value: u32) -> Result<(), LianaDescError> {
 //  - Be deriveable (to contain a wildcard)
 //  - Be multipath (to contain a step in the derivation path with multiple indexes)
 //  - The multipath step to only contain two indexes, 0 and 1.
-//  - Be 'signable' by an external signer (to contain an origin)
 fn is_valid_desc_key(key: &descriptor::DescriptorPublicKey) -> bool {
     match *key {
         descriptor::DescriptorPublicKey::Single(..) | descriptor::DescriptorPublicKey::XPub(..) => {
@@ -218,12 +217,7 @@ fn is_valid_desc_key(key: &descriptor::DescriptorPublicKey) -> bool {
             let der_paths = xpub.derivation_paths.paths();
             // Rust-miniscript enforces BIP389 which states that all paths must have the same len.
             let len = der_paths.get(0).expect("Cannot be empty").len();
-            // Technically the xpub could be for the master xpub and not have an origin. But it's
-            // no unlikely (and easily fixable) while users shooting themselves in the foot by
-            // forgetting to provide the origin is so likely that it's worth ruling out xpubs
-            // without origin entirely.
-            xpub.origin.is_some()
-                && xpub.wildcard == descriptor::Wildcard::Unhardened
+            xpub.wildcard == descriptor::Wildcard::Unhardened
                 && der_paths.len() == 2
                 && der_paths[0][len - 1] == 0.into()
                 && der_paths[1][len - 1] == 1.into()
@@ -975,32 +969,32 @@ mod tests {
 
     #[test]
     fn descriptor_creation() {
-        let owner_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap());
-        let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*").unwrap());
+        let owner_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap());
+        let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*").unwrap());
         let timelock = 52560;
-        assert_eq!(MultipathDescriptor::new(owner_key.clone(), heir_key.clone(), timelock).unwrap().to_string(), "wsh(or_d(pk([abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*),and_v(v:pkh([abcdef01]xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*),older(52560))))#g7vk9r5l");
+        assert_eq!(MultipathDescriptor::new(owner_key.clone(), heir_key.clone(), timelock).unwrap().to_string(), "wsh(or_d(pk(xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*),and_v(v:pkh(xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*),older(52560))))#8n2ydpkt");
 
         // A decaying multisig after 6 months. Note we can't duplicate the keys, so different ones
         // are used. In practice they would both be controlled by the same entity.
         let primary_keys = LianaDescKeys::from_multi(
             3,
             vec![
-                descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap(),
+                descriptor::DescriptorPublicKey::from_str("xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap(),
                 descriptor::DescriptorPublicKey::from_str("[aabb0011/10/4893]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*").unwrap(),
-                descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*").unwrap()
+                descriptor::DescriptorPublicKey::from_str("xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*").unwrap()
             ]
         )
         .unwrap();
         let recovery_keys = LianaDescKeys::from_multi(
             2,
             vec![
-                descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*").unwrap(),
-                descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*").unwrap(),
+                descriptor::DescriptorPublicKey::from_str("xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*").unwrap(),
+                descriptor::DescriptorPublicKey::from_str("xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*").unwrap(),
                 descriptor::DescriptorPublicKey::from_str("[aabb0011/10/4893]xpub6AyxexvxizZJffF153evmfqHcE9MV88fCNCAtP3jQjXJHwrAKri71Tq9jWUkPxj9pja4u6AkCPHY7atgxzSEa2HtDwJfrRWKK4fsfQg4o77/<0;1>/*").unwrap(),
             ],
         )
         .unwrap();
-        assert_eq!(MultipathDescriptor::new(primary_keys, recovery_keys, 26352).unwrap().to_string(), "wsh(or_d(multi(3,[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*,[aabb0011/10/4893]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*,[abcdef01]xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*),and_v(v:multi(2,[abcdef01]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*,[abcdef01]xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*,[aabb0011/10/4893]xpub6AyxexvxizZJffF153evmfqHcE9MV88fCNCAtP3jQjXJHwrAKri71Tq9jWUkPxj9pja4u6AkCPHY7atgxzSEa2HtDwJfrRWKK4fsfQg4o77/<0;1>/*),older(26352))))#s0zsa6uc");
+        assert_eq!(MultipathDescriptor::new(primary_keys, recovery_keys, 26352).unwrap().to_string(), "wsh(or_d(multi(3,xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*,[aabb0011/10/4893]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*,xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*),and_v(v:multi(2,xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*,xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*,[aabb0011/10/4893]xpub6AyxexvxizZJffF153evmfqHcE9MV88fCNCAtP3jQjXJHwrAKri71Tq9jWUkPxj9pja4u6AkCPHY7atgxzSEa2HtDwJfrRWKK4fsfQg4o77/<0;1>/*),older(26352))))#slaa6mlr");
 
         // We prevent footguns with timelocks by requiring a u16. Note how the following wouldn't
         // compile:
@@ -1012,54 +1006,54 @@ mod tests {
         MultipathDescriptor::new(owner_key, heir_key, 0).unwrap_err();
 
         let owner_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[aabb0011/10/4893]xpub661MyMwAqRbcFG59fiikD8UV762quhruT8K8bdjqy6N2o3LG7yohoCdLg1m2HAY1W6rfBrtauHkBhbfA4AQ3iazaJj5wVPhwgaRCHBW2DBg/<0;1>/*").unwrap());
-        let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/<0;1>/*").unwrap());
+        let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/<0;1>/*").unwrap());
         let timelock = 57600;
-        assert_eq!(MultipathDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap().to_string(), "wsh(or_d(pk([aabb0011/10/4893]xpub661MyMwAqRbcFG59fiikD8UV762quhruT8K8bdjqy6N2o3LG7yohoCdLg1m2HAY1W6rfBrtauHkBhbfA4AQ3iazaJj5wVPhwgaRCHBW2DBg/<0;1>/*),and_v(v:pkh([abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/<0;1>/*),older(57600))))#ak4cm093");
+        assert_eq!(MultipathDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap().to_string(), "wsh(or_d(pk([aabb0011/10/4893]xpub661MyMwAqRbcFG59fiikD8UV762quhruT8K8bdjqy6N2o3LG7yohoCdLg1m2HAY1W6rfBrtauHkBhbfA4AQ3iazaJj5wVPhwgaRCHBW2DBg/<0;1>/*),and_v(v:pkh(xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/<0;1>/*),older(57600))))#l6dlpc2l");
 
         // We can't pass a raw key, an xpub that is not deriveable, only hardened derivable,
         // without both the change and receive derivation paths, or with more than 2 different
         // derivation paths.
-        let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/<0;1>/354").unwrap());
+        let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/<0;1>/354").unwrap());
         MultipathDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap_err();
-        let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/<0;1>/*'").unwrap());
+        let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/<0;1>/*'").unwrap());
         MultipathDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap_err();
         let heir_key = LianaDescKeys::from_single(
             descriptor::DescriptorPublicKey::from_str(
-                "[abcdef01]02e24913be26dbcfdf8e8e94870b28725cdae09b448b6c127767bf0154e3a3c8e5",
+                "02e24913be26dbcfdf8e8e94870b28725cdae09b448b6c127767bf0154e3a3c8e5",
             )
             .unwrap(),
         );
         MultipathDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap_err();
-        let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/*'").unwrap());
+        let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/*'").unwrap());
         MultipathDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap_err();
-        let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/<0;1;2>/*'").unwrap());
+        let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/<0;1;2>/*'").unwrap());
         MultipathDescriptor::new(owner_key, heir_key, timelock).unwrap_err();
 
         // And it's checked even in a multisig. For instance:
         let primary_keys = LianaDescKeys::from_multi(
             1,
             vec![
-                descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap(),
-                descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/<0;1>/354").unwrap(),
+                descriptor::DescriptorPublicKey::from_str("xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap(),
+                descriptor::DescriptorPublicKey::from_str("xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/<0;1>/354").unwrap(),
             ]
         )
         .unwrap();
         let recovery_keys = LianaDescKeys::from_multi(
             1,
             vec![
-                descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*").unwrap(),
-                descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*").unwrap(),
+                descriptor::DescriptorPublicKey::from_str("xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*").unwrap(),
+                descriptor::DescriptorPublicKey::from_str("xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*").unwrap(),
             ],
         )
         .unwrap();
         MultipathDescriptor::new(primary_keys, recovery_keys, 26352).unwrap_err();
 
         // You can't pass duplicate keys, even if they are encoded differently.
-        let owner_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap());
-        let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap());
+        let owner_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap());
+        let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap());
         MultipathDescriptor::new(owner_key, heir_key, timelock).unwrap_err();
         let owner_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[00aabb44]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap());
-        let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap());
+        let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap());
         MultipathDescriptor::new(owner_key, heir_key, timelock).unwrap_err();
         let owner_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[00aabb44]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap());
         let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[11223344/2/98]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap());
@@ -1069,34 +1063,28 @@ mod tests {
         let primary_keys = LianaDescKeys::from_multi(
             3,
             vec![
-                descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap(),
-                descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*").unwrap(),
-                descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*").unwrap()
+                descriptor::DescriptorPublicKey::from_str("xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap(),
+                descriptor::DescriptorPublicKey::from_str("xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*").unwrap(),
+                descriptor::DescriptorPublicKey::from_str("xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*").unwrap()
             ]
         )
         .unwrap();
         let recovery_keys = LianaDescKeys::from_multi(
             2,
             vec![
-                descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*").unwrap(),
-                descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*").unwrap(),
-                descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*").unwrap(),
+                descriptor::DescriptorPublicKey::from_str("xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*").unwrap(),
+                descriptor::DescriptorPublicKey::from_str("xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*").unwrap(),
+                descriptor::DescriptorPublicKey::from_str("xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*").unwrap(),
             ],
         )
         .unwrap();
         MultipathDescriptor::new(primary_keys, recovery_keys, 26352).unwrap_err();
-
-        // No origin in one of the keys
-        let owner_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap());
-        let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*").unwrap());
-        let timelock = 52560;
-        MultipathDescriptor::new(owner_key, heir_key, timelock).unwrap_err();
     }
 
     #[test]
     fn inheritance_descriptor_derivation() {
         let secp = secp256k1::Secp256k1::verification_only();
-        let desc = MultipathDescriptor::from_str("wsh(andor(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#2qj59a9y").unwrap();
+        let desc = MultipathDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#5f6qd0d9").unwrap();
         let der_desc = desc.receive_descriptor().derive(11.into(), &secp);
         assert_eq!(
             "bc1q26gtczlz03u6juf5cxppapk4sr4fyz53s3g4zs2cgactcahqv6yqc2t8e6",
@@ -1111,19 +1099,19 @@ mod tests {
 
     #[test]
     fn inheritance_descriptor_tl_value() {
-        let desc = MultipathDescriptor::from_str("wsh(andor(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(1),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap();
+        let desc = MultipathDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(1),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap();
         assert_eq!(desc.timelock_value(), 1);
 
-        let desc = MultipathDescriptor::from_str("wsh(andor(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(42000),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap();
+        let desc = MultipathDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(42000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap();
         assert_eq!(desc.timelock_value(), 42000);
 
-        let desc = MultipathDescriptor::from_str("wsh(andor(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(65535),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap();
+        let desc = MultipathDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(65535),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap();
         assert_eq!(desc.timelock_value(), 0xffff);
     }
 
     #[test]
     fn inheritance_descriptor_sat_size() {
-        let desc = MultipathDescriptor::from_str("wsh(or_d(pk([92162c45]tpubD6NzVbkrYhZ4WzTf9SsD6h7AH7oQEippXK2KP8qvhMMqFoNeN5YFVi7vRyeRSDGtgd2bPyMxUNmHui8t5yCgszxPPxMafu1VVzDpg9aruYW/<0;1>/*),and_v(v:pkh([abcdef01]tpubD6NzVbkrYhZ4Wdgu2yfdmrce5g4fiH1ZLmKhewsnNKupbi4sxjH1ZVAorkBLWSkhsjhg8kiq8C4BrBjMy3SjAKDyDdbuvUa1ToAHbiR98js/<0;1>/*),older(2))))#ravw7jw5").unwrap();
+        let desc = MultipathDescriptor::from_str("wsh(or_d(pk([92162c45]tpubD6NzVbkrYhZ4WzTf9SsD6h7AH7oQEippXK2KP8qvhMMqFoNeN5YFVi7vRyeRSDGtgd2bPyMxUNmHui8t5yCgszxPPxMafu1VVzDpg9aruYW/<0;1>/*),and_v(v:pkh(tpubD6NzVbkrYhZ4Wdgu2yfdmrce5g4fiH1ZLmKhewsnNKupbi4sxjH1ZVAorkBLWSkhsjhg8kiq8C4BrBjMy3SjAKDyDdbuvUa1ToAHbiR98js/<0;1>/*),older(2))))#uact7s3g").unwrap();
         assert_eq!(desc.max_sat_vbytes(), (1 + 69 + 1 + 34 + 73 + 3) / 4); // See the stack details below.
 
         // Maximum input size is (txid + vout + scriptsig + nSequence + max_sat).
@@ -1149,8 +1137,8 @@ mod tests {
 
     #[test]
     fn liana_desc_keys() {
-        let desc_key_a = descriptor::DescriptorPublicKey::from_str("[aabbccdd]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap();
-        let desc_key_b = descriptor::DescriptorPublicKey::from_str("[aabbccdd]xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*").unwrap();
+        let desc_key_a = descriptor::DescriptorPublicKey::from_str("xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap();
+        let desc_key_b = descriptor::DescriptorPublicKey::from_str("xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*").unwrap();
         LianaDescKeys::from_single(desc_key_a.clone());
 
         LianaDescKeys::from_multi(1, vec![desc_key_a.clone()]).unwrap_err();
@@ -1172,13 +1160,13 @@ mod tests {
     #[test]
     fn roundtrip_descriptor() {
         // A descriptor with single keys in both primary and recovery paths
-        roundtrip("wsh(or_d(pk([aabbccdd]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*),and_v(v:pkh([aabbccdd]xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*),older(52560))))#7437yjrs");
+        roundtrip("wsh(or_d(pk(xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*),and_v(v:pkh(xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*),older(52560))))#8n2ydpkt");
         // One with a multisig in both paths
-        roundtrip("wsh(or_d(multi(3,[aabbccdd]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*,[aabb0011/10/4893]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*,[aabbccdd]xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*),and_v(v:multi(2,[aabbccdd]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*,[aabbccdd]xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*,[aabb0011/10/4893]xpub6AyxexvxizZJffF153evmfqHcE9MV88fCNCAtP3jQjXJHwrAKri71Tq9jWUkPxj9pja4u6AkCPHY7atgxzSEa2HtDwJfrRWKK4fsfQg4o77/<0;1>/*),older(26352))))#ypwt7h7e");
+        roundtrip("wsh(or_d(multi(3,xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*,[aabb0011/10/4893]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*,xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*),and_v(v:multi(2,xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*,xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*,[aabb0011/10/4893]xpub6AyxexvxizZJffF153evmfqHcE9MV88fCNCAtP3jQjXJHwrAKri71Tq9jWUkPxj9pja4u6AkCPHY7atgxzSEa2HtDwJfrRWKK4fsfQg4o77/<0;1>/*),older(26352))))#slaa6mlr");
         // A single key as primary path, a multisig as recovery
-        roundtrip("wsh(or_d(pk([aabbccdd]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*),and_v(v:multi(2,[aabbccdd]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*,[aabbccdd]xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*,[aabb0011/10/4893]xpub6AyxexvxizZJffF153evmfqHcE9MV88fCNCAtP3jQjXJHwrAKri71Tq9jWUkPxj9pja4u6AkCPHY7atgxzSEa2HtDwJfrRWKK4fsfQg4o77/<0;1>/*),older(26352))))#7du8x4v7");
+        roundtrip("wsh(or_d(pk(xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*),and_v(v:multi(2,xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*,xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*,[aabb0011/10/4893]xpub6AyxexvxizZJffF153evmfqHcE9MV88fCNCAtP3jQjXJHwrAKri71Tq9jWUkPxj9pja4u6AkCPHY7atgxzSEa2HtDwJfrRWKK4fsfQg4o77/<0;1>/*),older(26352))))#f5m0vfpf");
         // The other way around
-        roundtrip("wsh(or_d(multi(3,[aabbccdd]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*,[aabb0011/10/4893]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*,[aabbccdd]xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*),and_v(v:pk([aabbccdd]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*),older(26352))))#y9l4ldvr");
+        roundtrip("wsh(or_d(multi(3,xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*,[aabb0011/10/4893]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*,xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*),and_v(v:pk(xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*),older(26352))))#3f4xttt3");
     }
 
     fn psbt_from_str(psbt_str: &str) -> Psbt {
diff --git a/src/lib.rs b/src/lib.rs
index 38798945..78d0d940 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -612,7 +612,7 @@ mod tests {
         };
 
         // Create a dummy config with this bitcoind
-        let desc_str = "wsh(andor(pk([aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/<0;1>/*),older(10000),pk([aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/<0;1>/*)))#3xh8xmhn";
+        let desc_str = "wsh(andor(pk(xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/<0;1>/*),older(10000),pk(xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/<0;1>/*)))#yudtr0k5";
         let desc = MultipathDescriptor::from_str(desc_str).unwrap();
         let receive_desc = desc.receive_descriptor().clone();
         let change_desc = desc.change_descriptor().clone();


Test patch:
diff --git a/src/testutils.rs b/src/testutils.rs
index b07cdda7..d49d4180 100644
--- a/src/testutils.rs
+++ b/src/testutils.rs
@@ -390,8 +390,8 @@ impl DummyLiana {
             poll_interval_secs: time::Duration::from_secs(2),
         };
 
-        let owner_key = descriptors::LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/<0;1>/*").unwrap());
-        let heir_key = descriptors::LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/<0;1>/*").unwrap());
+        let owner_key = descriptors::LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/<0;1>/*").unwrap());
+        let heir_key = descriptors::LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/<0;1>/*").unwrap());
         let desc = descriptors::MultipathDescriptor::new(owner_key, heir_key, 10_000).unwrap();
         let config = Config {
             bitcoin_config,
diff --git a/tests/fixtures.py b/tests/fixtures.py
index 57d1036f..7365499a 100644
--- a/tests/fixtures.py
+++ b/tests/fixtures.py
@@ -126,7 +126,7 @@ def lianad(bitcoind, directory):
     )
     csv_value = 10
     main_desc = Descriptor.from_str(
-        f"wsh(or_d(pk([aabbccdd]{primary_xpub}/<0;1>/*),and_v(v:pkh([aabbccdd]{recovery_xpub}/<0;1>/*),older({csv_value}))))"
+        f"wsh(or_d(pk({primary_xpub}/<0;1>/*),and_v(v:pkh({recovery_xpub}/<0;1>/*),older({csv_value}))))"
     )
 
     lianad = Lianad(
@@ -150,7 +150,7 @@ def lianad(bitcoind, directory):
 def multi_expression(thresh, keys):
     exp = f"multi({thresh},"
     for i, key in enumerate(keys):
-        exp += f"[aabbccdd]{key.get_xpub()}/<0;1>/*"
+        exp += f"{key.get_xpub()}/<0;1>/*"
         if i != len(keys) - 1:
             exp += ","
     return exp + ")"