From f13cd1fe73f2fc1afda0a4f35cac7f4840e5c7c8 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Wed, 22 Jan 2025 04:33:53 +0100 Subject: [PATCH 01/44] export: rename 'State' into 'Export' and separate export logic --- liana-gui/src/app/state/export.rs | 8 +- liana-gui/src/export.rs | 386 +++++++++++++++++------------- 2 files changed, 219 insertions(+), 175 deletions(-) diff --git a/liana-gui/src/app/state/export.rs b/liana-gui/src/app/state/export.rs index 193e6e3c..1f4c5459 100644 --- a/liana-gui/src/app/state/export.rs +++ b/liana-gui/src/app/state/export.rs @@ -13,7 +13,7 @@ use crate::{ view::{self, export::export_modal}, }, daemon::Daemon, - export::{self, get_path, ExportMessage, ExportProgress, ExportState}, + export::{self, get_path, ExportMessage, ExportProgress, ExportState, ExportType}, }; #[derive(Debug)] @@ -111,7 +111,11 @@ impl ExportModal { ExportState::Started | ExportState::Progress(_) => { Some(iced::Subscription::run_with_id( "transactions", - export::export_subscription(self.daemon.clone(), path.to_path_buf()), + export::export_subscription( + self.daemon.clone(), + path.to_path_buf(), + ExportType::Transactions, + ), )) } _ => None, diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index ef6090ce..8e87bd17 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -7,7 +7,7 @@ use std::{ mpsc::{channel, Receiver, Sender}, Arc, Mutex, }, - time::{self}, + time, }; use chrono::{DateTime, Duration, Utc}; @@ -95,6 +95,13 @@ pub enum Error { TxTimeMissing, } +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum ExportType { + Transactions, + Psbt, + Descriptor, +} + impl From for Error { fn from(value: JoinError) -> Self { Error::JoinError(format!("{:?}", value)) @@ -130,203 +137,58 @@ pub enum ExportProgress { None, } -pub struct State { +pub struct Export { pub receiver: Receiver, pub sender: Option>, pub handle: Option>>>, pub daemon: Arc, pub path: Box, + pub export_type: ExportType, } -impl State { - pub fn new(daemon: Arc, path: Box) -> Self { +impl Export { + pub fn new( + daemon: Arc, + path: Box, + export_type: ExportType, + ) -> Self { let (sender, receiver) = channel(); - State { + Export { receiver, sender: Some(sender), handle: None, daemon, path, + export_type, } } + pub async fn export_logic( + export_type: ExportType, + sender: Sender, + daemon: Arc, + path: PathBuf, + ) { + match export_type { + ExportType::Transactions => export_transactions(sender, daemon, path).await, + ExportType::Psbt => todo!(), + ExportType::Descriptor => todo!(), + }; + } + pub async fn start(&mut self) { if let (true, Some(sender)) = (self.handle.is_none(), self.sender.take()) { let daemon = self.daemon.clone(); let path = self.path.clone(); let cloned_sender = sender.clone(); + let export_type = self.export_type; let handle = tokio::spawn(async move { - let dir = match path.parent() { - Some(dir) => dir, - None => { - send_error!(sender, NoParentDir); - return; - } - }; - if !dir.exists() { - if let Err(e) = fs::create_dir_all(dir) { - send_error!(sender, e.into()); - return; - } - } - let mut file = match File::create(path.as_path()) { - Ok(f) => f, - Err(e) => { - send_error!(sender, e.into()); - return; - } - }; - - let header = "Date,Label,Value,Fee,Txid,Block\n".to_string(); - if let Err(e) = file.write_all(header.as_bytes()) { - send_error!(sender, e.into()); - return; - } - - // look 2 hour forward - // https://github.com/bitcoin/bitcoin/blob/62bd61de110b057cbfd6e31e4d0b727d93119c72/src/chain.h#L29 - let mut end = ((Utc::now() + Duration::hours(2)).timestamp()) as u32; - let total_txs = daemon.list_confirmed_txs(0, end, u32::MAX as u64).await; - let total_txs = match total_txs { - Ok(r) => r.transactions.len(), - Err(e) => { - send_error!(sender, e.into()); - return; - } - }; - - if total_txs == 0 { - send_progress!(sender, Ended); - } else { - send_progress!(sender, Progress(5.0)); - } - - let max = match daemon.backend() { - DaemonBackend::RemoteBackend => DEFAULT_LIMIT as u64, - _ => u32::MAX as u64, - }; - - // store txs in a map to avoid duplicates - let mut map = HashMap::::new(); - let mut limit = max; - - loop { - let history = daemon.list_history_txs(0, end, limit).await; - let history_txs = match history { - Ok(h) => h, - Err(e) => { - send_error!(sender, e.into()); - return; - } - }; - let dl = map.len() + history_txs.len(); - if dl > 0 { - let progress = (dl as f32) / (total_txs as f32) * 80.0; - send_progress!(sender, Progress(progress)); - } - // all txs have been fetched - if history_txs.is_empty() { - break; - } - if history_txs.len() == limit as usize { - let first = if let Some(t) = history_txs.first().expect("checked").time { - t - } else { - send_error!(sender, TxTimeMissing); - return; - }; - let last = if let Some(t) = history_txs.last().expect("checked").time { - t - } else { - send_error!(sender, TxTimeMissing); - return; - }; - // limit too low, all tx are in the same timestamp - // we must increase limit and retry - if first == last { - limit += DEFAULT_LIMIT as u64; - continue; - } else { - // add txs to map - for tx in history_txs { - let txid = tx.txid; - map.insert(txid, tx); - } - limit = max; - end = first.min(last); - continue; - } - } else - /* history_txs.len() < limit */ - { - // add txs to map - for tx in history_txs { - let txid = tx.txid; - map.insert(txid, tx); - } - break; - } - } - - let mut txs: Vec<_> = map.into_values().collect(); - txs.sort_by(|a, b| b.compare(a)); - - for mut tx in txs { - let date_time = tx - .time - .map(|t| { - let mut str = DateTime::from_timestamp(t as i64, 0) - .expect("bitcoin timestamp") - .to_rfc3339(); - //str has the form `1996-12-19T16:39:57-08:00` - // ^ ^^^^^^ - // replace `T` by ` `| | drop this part - str = str.replace("T", " "); - str[0..(str.len() - 6)].to_string() - }) - .unwrap_or("".to_string()); - - let txid = tx.txid.clone().to_string(); - let txid_label = tx.labels().get(&txid).cloned(); - let mut label = if let Some(txid) = txid_label { - txid - } else { - "".to_string() - }; - if !label.is_empty() { - label = format!("\"{}\"", label); - } - let txid = tx.txid.to_string(); - let fee = tx.fee_amount.unwrap_or(Amount::ZERO).to_sat() as i128; - let mut inputs_amount = 0; - tx.coins.iter().for_each(|(_, coin)| { - inputs_amount += coin.amount.to_sat() as i128; - }); - let value = tx.incoming_amount.to_sat() as i128 - inputs_amount; - let value = value as f64 / 100_000_000.0; - let fee = fee as f64 / 100_000_000.0; - let block = tx.height.map(|h| h.to_string()).unwrap_or("".to_string()); - let fee = if fee != 0.0 { - fee.to_string() - } else { - "".into() - }; - - let line = format!( - "{},{},{},{},{},{}\n", - date_time, label, value, fee, txid, block - ); - if let Err(e) = file.write_all(line.as_bytes()) { - send_error!(sender, e.into()); - return; - } - } - send_progress!(sender, Progress(100.0)); - send_progress!(sender, Ended); + Self::export_logic(export_type, cloned_sender, daemon, *path).await; }); let handle = Arc::new(Mutex::new(handle)); + let cloned_sender = sender.clone(); // we send the handle to the GUI so we can kill the thread on timeout // or user cancel action send_progress!(cloned_sender, Started(handle.clone())); @@ -348,9 +210,10 @@ impl State { pub fn export_subscription( daemon: Arc, path: PathBuf, + export_type: ExportType, ) -> impl Stream { iced::stream::channel(100, move |mut output| async move { - let mut state = State::new(daemon, Box::new(path)); + let mut state = Export::new(daemon, Box::new(path), export_type); loop { match state.state() { Status::Init => { @@ -407,6 +270,183 @@ pub fn export_subscription( }) } +pub async fn export_transactions( + sender: Sender, + daemon: Arc, + path: PathBuf, +) { + async move { + let dir = match path.parent() { + Some(dir) => dir, + None => { + send_error!(sender, NoParentDir); + return; + } + }; + if !dir.exists() { + if let Err(e) = fs::create_dir_all(dir) { + send_error!(sender, e.into()); + return; + } + } + let mut file = match File::create(path.as_path()) { + Ok(f) => f, + Err(e) => { + send_error!(sender, e.into()); + return; + } + }; + + let header = "Date,Label,Value,Fee,Txid,Block\n".to_string(); + if let Err(e) = file.write_all(header.as_bytes()) { + send_error!(sender, e.into()); + return; + } + + // look 2 hour forward + // https://github.com/bitcoin/bitcoin/blob/62bd61de110b057cbfd6e31e4d0b727d93119c72/src/chain.h#L29 + let mut end = ((Utc::now() + Duration::hours(2)).timestamp()) as u32; + let total_txs = daemon.list_confirmed_txs(0, end, u32::MAX as u64).await; + let total_txs = match total_txs { + Ok(r) => r.transactions.len(), + Err(e) => { + send_error!(sender, e.into()); + return; + } + }; + + if total_txs == 0 { + send_progress!(sender, Ended); + } else { + send_progress!(sender, Progress(5.0)); + } + + let max = match daemon.backend() { + DaemonBackend::RemoteBackend => DEFAULT_LIMIT as u64, + _ => u32::MAX as u64, + }; + + // store txs in a map to avoid duplicates + let mut map = HashMap::::new(); + let mut limit = max; + + loop { + let history = daemon.list_history_txs(0, end, limit).await; + let history_txs = match history { + Ok(h) => h, + Err(e) => { + send_error!(sender, e.into()); + return; + } + }; + let dl = map.len() + history_txs.len(); + if dl > 0 { + let progress = (dl as f32) / (total_txs as f32) * 80.0; + send_progress!(sender, Progress(progress)); + } + // all txs have been fetched + if history_txs.is_empty() { + break; + } + if history_txs.len() == limit as usize { + let first = if let Some(t) = history_txs.first().expect("checked").time { + t + } else { + send_error!(sender, TxTimeMissing); + return; + }; + let last = if let Some(t) = history_txs.last().expect("checked").time { + t + } else { + send_error!(sender, TxTimeMissing); + return; + }; + // limit too low, all tx are in the same timestamp + // we must increase limit and retry + if first == last { + limit += DEFAULT_LIMIT as u64; + continue; + } else { + // add txs to map + for tx in history_txs { + let txid = tx.txid; + map.insert(txid, tx); + } + limit = max; + end = first.min(last); + continue; + } + } else + /* history_txs.len() < limit */ + { + // add txs to map + for tx in history_txs { + let txid = tx.txid; + map.insert(txid, tx); + } + break; + } + } + + let mut txs: Vec<_> = map.into_values().collect(); + txs.sort_by(|a, b| b.compare(a)); + + for mut tx in txs { + let date_time = tx + .time + .map(|t| { + let mut str = DateTime::from_timestamp(t as i64, 0) + .expect("bitcoin timestamp") + .to_rfc3339(); + //str has the form `1996-12-19T16:39:57-08:00` + // ^ ^^^^^^ + // replace `T` by ` `| | drop this part + str = str.replace("T", " "); + str[0..(str.len() - 6)].to_string() + }) + .unwrap_or("".to_string()); + + let txid = tx.txid.clone().to_string(); + let txid_label = tx.labels().get(&txid).cloned(); + let mut label = if let Some(txid) = txid_label { + txid + } else { + "".to_string() + }; + if !label.is_empty() { + label = format!("\"{}\"", label); + } + let txid = tx.txid.to_string(); + let fee = tx.fee_amount.unwrap_or(Amount::ZERO).to_sat() as i128; + let mut inputs_amount = 0; + tx.coins.iter().for_each(|(_, coin)| { + inputs_amount += coin.amount.to_sat() as i128; + }); + let value = tx.incoming_amount.to_sat() as i128 - inputs_amount; + let value = value as f64 / 100_000_000.0; + let fee = fee as f64 / 100_000_000.0; + let block = tx.height.map(|h| h.to_string()).unwrap_or("".to_string()); + let fee = if fee != 0.0 { + fee.to_string() + } else { + "".into() + }; + + let line = format!( + "{},{},{},{},{},{}\n", + date_time, label, value, fee, txid, block + ); + if let Err(e) = file.write_all(line.as_bytes()) { + send_error!(sender, e.into()); + return; + } + } + send_progress!(sender, Progress(100.0)); + send_progress!(sender, Ended); + } + .await; +} + pub async fn get_path() -> Option { let date = chrono::Local::now().format("%Y-%m-%dT%H-%M-%S"); let file_name = format!("liana-txs-{date}.csv"); From a3bf250696c7305e2af2667792ba6c1d02e8a111 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Wed, 22 Jan 2025 05:10:15 +0100 Subject: [PATCH 02/44] gui: make 'ExportModal' generic --- liana-gui/src/app/state/export.rs | 17 +++++++++++++++-- liana-gui/src/app/state/transactions.rs | 7 +++++-- liana-gui/src/export.rs | 6 ++---- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/liana-gui/src/app/state/export.rs b/liana-gui/src/app/state/export.rs index 1f4c5459..d1871d52 100644 --- a/liana-gui/src/app/state/export.rs +++ b/liana-gui/src/app/state/export.rs @@ -23,22 +23,35 @@ pub struct ExportModal { state: ExportState, error: Option, daemon: Arc, + export_type: ExportType, } impl ExportModal { #[allow(clippy::new_without_default)] - pub fn new(daemon: Arc) -> Self { + pub fn new(daemon: Arc, export_type: ExportType) -> Self { Self { path: None, handle: None, state: ExportState::Init, error: None, daemon, + export_type, + } + } + + pub fn default_filename(&self) -> String { + match self.export_type { + ExportType::Transactions => { + let date = chrono::Local::now().format("%Y-%m-%dT%H-%M-%S"); + format!("liana-txs-{date}.csv") + } + ExportType::Psbt => todo!(), + ExportType::Descriptor => todo!(), } } pub fn launch(&self) -> Task { - Task::perform(get_path(), |m| { + Task::perform(get_path(self.default_filename()), |m| { app::message::Message::View(view::Message::Export(ExportMessage::Path(m))) }) } diff --git a/liana-gui/src/app/state/transactions.rs b/liana-gui/src/app/state/transactions.rs index a8a0d4fd..c0e4c7f8 100644 --- a/liana-gui/src/app/state/transactions.rs +++ b/liana-gui/src/app/state/transactions.rs @@ -28,7 +28,7 @@ use crate::{ wallet::Wallet, }, daemon::model::{self, LabelsLoader}, - export::ExportMessage, + export::{ExportMessage, ExportType}, }; use crate::daemon::{ @@ -268,7 +268,10 @@ impl State for TransactionsPanel { } Message::View(view::Message::Export(ExportMessage::Open)) => { if let TransactionsModal::None = &self.modal { - self.modal = TransactionsModal::Export(ExportModal::new(daemon)); + self.modal = TransactionsModal::Export(ExportModal::new( + daemon, + ExportType::Transactions, + )); if let TransactionsModal::Export(m) = &self.modal { return m.launch(); } diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index 8e87bd17..22f0799d 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -447,12 +447,10 @@ pub async fn export_transactions( .await; } -pub async fn get_path() -> Option { - let date = chrono::Local::now().format("%Y-%m-%dT%H-%M-%S"); - let file_name = format!("liana-txs-{date}.csv"); +pub async fn get_path(filename: String) -> Option { rfd::AsyncFileDialog::new() .set_title("Choose a location to export...") - .set_file_name(file_name) + .set_file_name(filename) .save_file() .await .map(|fh| fh.path().to_path_buf()) From baf4e75efe3b1150a4a8277f3ed8b1f265f75cdd Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Mon, 10 Mar 2025 15:57:54 +0100 Subject: [PATCH 03/44] export: implement export for descriptor --- liana-gui/src/app/state/export.rs | 14 ++++-- liana-gui/src/export.rs | 79 ++++++++++++++++++++----------- 2 files changed, 63 insertions(+), 30 deletions(-) diff --git a/liana-gui/src/app/state/export.rs b/liana-gui/src/app/state/export.rs index d1871d52..2d12d0fa 100644 --- a/liana-gui/src/app/state/export.rs +++ b/liana-gui/src/app/state/export.rs @@ -40,13 +40,21 @@ impl ExportModal { } pub fn default_filename(&self) -> String { - match self.export_type { + match &self.export_type { ExportType::Transactions => { let date = chrono::Local::now().format("%Y-%m-%dT%H-%M-%S"); format!("liana-txs-{date}.csv") } - ExportType::Psbt => todo!(), - ExportType::Descriptor => todo!(), + ExportType::Psbt(_) => todo!(), + ExportType::Descriptor(descriptor) => { + let checksum = descriptor + .to_string() + .split_once('#') + .map(|(_, checksum)| checksum) + .expect("cannot fail") + .to_string(); + format!("liana-{}.descriptor", checksum) + } } } diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index 22f0799d..6dbd1701 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -11,7 +11,10 @@ use std::{ }; use chrono::{DateTime, Duration, Utc}; -use liana::miniscript::bitcoin::{Amount, Txid}; +use liana::{ + descriptors::LianaDescriptor, + miniscript::bitcoin::{Amount, Txid}, +}; use tokio::{ task::{JoinError, JoinHandle}, time::sleep, @@ -54,6 +57,31 @@ macro_rules! send_progress { }; } +macro_rules! open_file { + ($path:ident, $sender:ident) => {{ + let dir = match $path.parent() { + Some(dir) => dir, + None => { + send_error!($sender, NoParentDir); + return; + } + }; + if !dir.exists() { + if let Err(e) = fs::create_dir_all(dir) { + send_error!($sender, e.into()); + return; + } + } + match File::create($path.as_path()) { + Ok(f) => f, + Err(e) => { + send_error!($sender, e.into()); + return; + } + } + }}; +} + #[derive(Debug, Clone)] pub enum ExportMessage { Open, @@ -95,11 +123,11 @@ pub enum Error { TxTimeMissing, } -#[derive(Debug, PartialEq, Clone, Copy)] +#[derive(Debug, PartialEq, Clone)] pub enum ExportType { Transactions, - Psbt, - Descriptor, + Psbt(String), + Descriptor(LianaDescriptor), } impl From for Error { @@ -171,8 +199,8 @@ impl Export { ) { match export_type { ExportType::Transactions => export_transactions(sender, daemon, path).await, - ExportType::Psbt => todo!(), - ExportType::Descriptor => todo!(), + ExportType::Psbt(_) => todo!(), + ExportType::Descriptor(descriptor) => export_descriptor(sender, path, descriptor), }; } @@ -182,7 +210,7 @@ impl Export { let path = self.path.clone(); let cloned_sender = sender.clone(); - let export_type = self.export_type; + let export_type = self.export_type.clone(); let handle = tokio::spawn(async move { Self::export_logic(export_type, cloned_sender, daemon, *path).await; }); @@ -276,26 +304,7 @@ pub async fn export_transactions( path: PathBuf, ) { async move { - let dir = match path.parent() { - Some(dir) => dir, - None => { - send_error!(sender, NoParentDir); - return; - } - }; - if !dir.exists() { - if let Err(e) = fs::create_dir_all(dir) { - send_error!(sender, e.into()); - return; - } - } - let mut file = match File::create(path.as_path()) { - Ok(f) => f, - Err(e) => { - send_error!(sender, e.into()); - return; - } - }; + let mut file = open_file!(path, sender); let header = "Date,Label,Value,Fee,Txid,Block\n".to_string(); if let Err(e) = file.write_all(header.as_bytes()) { @@ -447,6 +456,22 @@ pub async fn export_transactions( .await; } +pub fn export_descriptor( + sender: Sender, + path: PathBuf, + descriptor: LianaDescriptor, +) { + let mut file = open_file!(path, sender); + + let descr_string = descriptor.to_string(); + if let Err(e) = file.write_all(descr_string.as_bytes()) { + send_error!(sender, e.into()); + return; + } + send_progress!(sender, Progress(100.0)); + send_progress!(sender, Ended); +} + pub async fn get_path(filename: String) -> Option { rfd::AsyncFileDialog::new() .set_title("Choose a location to export...") From 2b9324993a92ecb082d59c6aa013e6136b025aba Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Wed, 22 Jan 2025 07:33:34 +0100 Subject: [PATCH 04/44] export: implement export for psbt --- liana-gui/src/app/state/export.rs | 2 +- liana-gui/src/export.rs | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/liana-gui/src/app/state/export.rs b/liana-gui/src/app/state/export.rs index 2d12d0fa..4c4954a9 100644 --- a/liana-gui/src/app/state/export.rs +++ b/liana-gui/src/app/state/export.rs @@ -45,7 +45,7 @@ impl ExportModal { let date = chrono::Local::now().format("%Y-%m-%dT%H-%M-%S"); format!("liana-txs-{date}.csv") } - ExportType::Psbt(_) => todo!(), + ExportType::Psbt(_) => "psbt.psbt".into(), ExportType::Descriptor(descriptor) => { let checksum = descriptor .to_string() diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index 6dbd1701..5a3cd65f 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -199,7 +199,7 @@ impl Export { ) { match export_type { ExportType::Transactions => export_transactions(sender, daemon, path).await, - ExportType::Psbt(_) => todo!(), + ExportType::Psbt(psbt) => export_psbt(sender, path, psbt), ExportType::Descriptor(descriptor) => export_descriptor(sender, path, descriptor), }; } @@ -472,6 +472,17 @@ pub fn export_descriptor( send_progress!(sender, Ended); } +pub fn export_psbt(sender: Sender, path: PathBuf, psbt: String) { + let mut file = open_file!(path, sender); + + if let Err(e) = file.write_all(psbt.as_bytes()) { + send_error!(sender, e.into()); + return; + } + send_progress!(sender, Progress(100.0)); + send_progress!(sender, Ended); +} + pub async fn get_path(filename: String) -> Option { rfd::AsyncFileDialog::new() .set_title("Choose a location to export...") From 158651ebe7fbbf07fa8f8fbd74aa927ea53034ce Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Thu, 30 Jan 2025 07:22:18 +0100 Subject: [PATCH 05/44] import: implement import from file for psbt & descriptor --- liana-gui/src/app/message.rs | 4 +- liana-gui/src/app/state/export.rs | 90 +++++++++------ liana-gui/src/app/state/transactions.rs | 14 ++- liana-gui/src/app/view/export.rs | 45 ++++---- liana-gui/src/app/view/message.rs | 4 +- liana-gui/src/app/view/transactions.rs | 7 +- liana-gui/src/export.rs | 145 ++++++++++++++++-------- 7 files changed, 194 insertions(+), 115 deletions(-) diff --git a/liana-gui/src/app/message.rs b/liana-gui/src/app/message.rs index dad49c93..96c68dc6 100644 --- a/liana-gui/src/app/message.rs +++ b/liana-gui/src/app/message.rs @@ -11,7 +11,7 @@ use lianad::config::Config as DaemonConfig; use crate::{ app::{cache::Cache, error::Error, view, wallet::Wallet}, daemon::model::*, - export::ExportMessage, + export::ImportExportMessage, hw::HardwareWalletMessage, }; @@ -47,5 +47,5 @@ pub enum Message { LabelsUpdated(Result>, Error>), BroadcastModal(Result, Error>), RbfModal(Box, bool, Result, Error>), - Export(ExportMessage), + Export(ImportExportMessage), } diff --git a/liana-gui/src/app/state/export.rs b/liana-gui/src/app/state/export.rs index 4c4954a9..e133f002 100644 --- a/liana-gui/src/app/state/export.rs +++ b/liana-gui/src/app/state/export.rs @@ -13,40 +13,40 @@ use crate::{ view::{self, export::export_modal}, }, daemon::Daemon, - export::{self, get_path, ExportMessage, ExportProgress, ExportState, ExportType}, + export::{self, get_path, ImportExportMessage, ImportExportState, ImportExportType, Progress}, }; #[derive(Debug)] pub struct ExportModal { path: Option, handle: Option>>>, - state: ExportState, + state: ImportExportState, error: Option, daemon: Arc, - export_type: ExportType, + import_export_type: ImportExportType, } impl ExportModal { #[allow(clippy::new_without_default)] - pub fn new(daemon: Arc, export_type: ExportType) -> Self { + pub fn new(daemon: Arc, export_type: ImportExportType) -> Self { Self { path: None, handle: None, - state: ExportState::Init, + state: ImportExportState::Init, error: None, daemon, - export_type, + import_export_type: export_type, } } pub fn default_filename(&self) -> String { - match &self.export_type { - ExportType::Transactions => { + match &self.import_export_type { + ImportExportType::Transactions => { let date = chrono::Local::now().format("%Y-%m-%dT%H-%M-%S"); format!("liana-txs-{date}.csv") } - ExportType::Psbt(_) => "psbt.psbt".into(), - ExportType::Descriptor(descriptor) => { + ImportExportType::ExportPsbt(_) => "psbt.psbt".into(), + ImportExportType::Descriptor(descriptor) => { let checksum = descriptor .to_string() .split_once('#') @@ -55,48 +55,64 @@ impl ExportModal { .to_string(); format!("liana-{}.descriptor", checksum) } + ImportExportType::ImportPsbt => "psbt.psbt".into(), + ImportExportType::ImportDescriptor => "descriptor.descriptor".into(), } } pub fn launch(&self) -> Task { Task::perform(get_path(self.default_filename()), |m| { - app::message::Message::View(view::Message::Export(ExportMessage::Path(m))) + app::message::Message::View(view::Message::ImportExport(ImportExportMessage::Path(m))) }) } - pub fn update(&mut self, message: ExportMessage) -> Task { + pub fn update(&mut self, message: ImportExportMessage) -> Task { match message { - ExportMessage::ExportProgress(m) => match m { - ExportProgress::Started(handle) => { + ImportExportMessage::Progress(m) => match m { + Progress::Started(handle) => { self.handle = Some(handle); - self.state = ExportState::Progress(0.0); + self.state = ImportExportState::Progress(0.0); } - ExportProgress::Progress(p) => { - if let ExportState::Progress(_) = self.state { - self.state = ExportState::Progress(p); + Progress::Progress(p) => { + if let ImportExportState::Progress(_) = self.state { + self.state = ImportExportState::Progress(p); } } - ExportProgress::Finished | ExportProgress::Ended => self.state = ExportState::Ended, - ExportProgress::Error(e) => self.error = Some(e), - ExportProgress::None => {} + Progress::Finished | Progress::Ended => self.state = ImportExportState::Ended, + Progress::Error(e) => self.error = Some(e), + Progress::None => {} + Progress::Psbt(_) => { + if self.import_export_type == ImportExportType::ImportPsbt { + self.state = ImportExportState::Ended; + } + // TODO: forward PSBT + } + Progress::Descriptor(_) => { + if self.import_export_type == ImportExportType::ImportDescriptor { + self.state = ImportExportState::Ended; + } + // TODO: forward Descriptor + } }, - ExportMessage::TimedOut => { - self.stop(ExportState::TimedOut); + ImportExportMessage::TimedOut => { + self.stop(ImportExportState::TimedOut); } - ExportMessage::UserStop => { - self.stop(ExportState::Aborted); + ImportExportMessage::UserStop => { + self.stop(ImportExportState::Aborted); } - ExportMessage::Path(p) => { + ImportExportMessage::Path(p) => { if let Some(path) = p { self.path = Some(path); self.start(); } else { return Task::perform(async {}, |_| { - app::message::Message::View(view::Message::Export(ExportMessage::Close)) + app::message::Message::View(view::Message::ImportExport( + ImportExportMessage::Close, + )) }); } } - ExportMessage::Close | ExportMessage::Open => { /* unreachable */ } + ImportExportMessage::Close | ImportExportMessage::Open => { /* unreachable */ } } Task::none() } @@ -106,36 +122,36 @@ impl ExportModal { export_modal(&self.state, self.error.as_ref(), "Transactions"), ); match self.state { - ExportState::TimedOut - | ExportState::Aborted - | ExportState::Ended - | ExportState::Closed => modal.on_blur(Some(view::Message::Close)), + ImportExportState::TimedOut + | ImportExportState::Aborted + | ImportExportState::Ended + | ImportExportState::Closed => modal.on_blur(Some(view::Message::Close)), _ => modal, } .into() } pub fn start(&mut self) { - self.state = ExportState::Started; + self.state = ImportExportState::Started; } - pub fn stop(&mut self, state: ExportState) { + pub fn stop(&mut self, state: ImportExportState) { if let Some(handle) = self.handle.take() { handle.lock().expect("poisoned").abort(); self.state = state; } } - pub fn subscription(&self) -> Option> { + pub fn subscription(&self) -> Option> { if let Some(path) = &self.path { match &self.state { - ExportState::Started | ExportState::Progress(_) => { + ImportExportState::Started | ImportExportState::Progress(_) => { Some(iced::Subscription::run_with_id( "transactions", export::export_subscription( self.daemon.clone(), path.to_path_buf(), - ExportType::Transactions, + self.import_export_type.clone(), ), )) } diff --git a/liana-gui/src/app/state/transactions.rs b/liana-gui/src/app/state/transactions.rs index c0e4c7f8..dfbf4353 100644 --- a/liana-gui/src/app/state/transactions.rs +++ b/liana-gui/src/app/state/transactions.rs @@ -28,7 +28,7 @@ use crate::{ wallet::Wallet, }, daemon::model::{self, LabelsLoader}, - export::{ExportMessage, ExportType}, + export::{ImportExportMessage, ImportExportType}, }; use crate::daemon::{ @@ -266,18 +266,18 @@ impl State for TransactionsPanel { ); } } - Message::View(view::Message::Export(ExportMessage::Open)) => { + Message::View(view::Message::ImportExport(ImportExportMessage::Open)) => { if let TransactionsModal::None = &self.modal { self.modal = TransactionsModal::Export(ExportModal::new( daemon, - ExportType::Transactions, + ImportExportType::Transactions, )); if let TransactionsModal::Export(m) = &self.modal { return m.launch(); } } } - Message::View(view::Message::Export(ExportMessage::Close)) => { + Message::View(view::Message::ImportExport(ImportExportMessage::Close)) => { if let TransactionsModal::Export(_) = &self.modal { self.modal = TransactionsModal::None; } @@ -286,7 +286,7 @@ impl State for TransactionsPanel { return match &mut self.modal { TransactionsModal::CreateRbf(modal) => modal.update(daemon, _cache, message), TransactionsModal::Export(modal) => { - if let Message::View(view::Message::Export(m)) = msg { + if let Message::View(view::Message::ImportExport(m)) = msg { modal.update(m.clone()) } else { Task::none() @@ -330,7 +330,9 @@ impl State for TransactionsPanel { if let TransactionsModal::Export(modal) = &self.modal { if let Some(sub) = modal.subscription() { return sub.map(|m| { - Message::View(view::Message::Export(ExportMessage::ExportProgress(m))) + Message::View(view::Message::ImportExport(ImportExportMessage::Progress( + m, + ))) }); } } diff --git a/liana-gui/src/app/view/export.rs b/liana-gui/src/app/view/export.rs index af9e9b2d..900cee8b 100644 --- a/liana-gui/src/app/view/export.rs +++ b/liana-gui/src/app/view/export.rs @@ -11,21 +11,21 @@ use liana_ui::{ widget::Element, }; -use crate::export::{Error, ExportMessage}; -use crate::{app::view::message::Message, export::ExportState}; +use crate::export::{Error, ImportExportMessage}; +use crate::{app::view::message::Message, export::ImportExportState}; /// Return the modal view for an export task pub fn export_modal<'a>( - state: &ExportState, + state: &ImportExportState, error: Option<&'a Error>, export_type: &str, ) -> Element<'a, Message> { let button = match state { - ExportState::Started | ExportState::Progress(_) => { - Some(button::secondary(None, "Cancel").on_press(ExportMessage::UserStop.into())) + ImportExportState::Started | ImportExportState::Progress(_) => { + Some(button::secondary(None, "Cancel").on_press(ImportExportMessage::UserStop.into())) } - ExportState::Ended | ExportState::TimedOut | ExportState::Aborted => { - Some(button::secondary(None, "Close").on_press(ExportMessage::Close.into())) + ImportExportState::Ended | ImportExportState::TimedOut | ImportExportState::Aborted => { + Some(button::secondary(None, "Close").on_press(ImportExportMessage::Close.into())) } _ => None, }; @@ -33,26 +33,29 @@ pub fn export_modal<'a>( format!("{:?}", error) } else { match state { - ExportState::Init => "".to_string(), - ExportState::ChoosePath => { + ImportExportState::Init => "".to_string(), + ImportExportState::ChoosePath => { "Select the path you want to export in the popup window...".into() } - ExportState::Path(_) => "".into(), - ExportState::Started => "Starting export...".into(), - ExportState::Progress(p) => format!("Progress: {}%", p.round()), - ExportState::TimedOut => "Export failed: timeout".into(), - ExportState::Aborted => "Export canceled".into(), - ExportState::Ended => "Export successful!".into(), - ExportState::Closed => "".into(), + ImportExportState::Path(_) => "".into(), + ImportExportState::Started => "Starting export...".into(), + ImportExportState::Progress(p) => format!("Progress: {}%", p.round()), + ImportExportState::TimedOut => "Export failed: timeout".into(), + ImportExportState::Aborted => "Export canceled".into(), + ImportExportState::Ended => "Export successful!".into(), + ImportExportState::Closed => "".into(), } }; let p = match state { - ExportState::Init => 0.0, - ExportState::ChoosePath | ExportState::Path(_) | ExportState::Started => 5.0, - ExportState::Progress(p) => *p, - ExportState::TimedOut | ExportState::Aborted | ExportState::Ended | ExportState::Closed => { - 100.0 + ImportExportState::Init => 0.0, + ImportExportState::ChoosePath | ImportExportState::Path(_) | ImportExportState::Started => { + 5.0 } + ImportExportState::Progress(p) => *p, + ImportExportState::TimedOut + | ImportExportState::Aborted + | ImportExportState::Ended + | ImportExportState::Closed => 100.0, }; let progress_bar_row = Row::new() .push(Space::with_width(30)) diff --git a/liana-gui/src/app/view/message.rs b/liana-gui/src/app/view/message.rs index 345dd1dd..a1e796c8 100644 --- a/liana-gui/src/app/view/message.rs +++ b/liana-gui/src/app/view/message.rs @@ -1,4 +1,4 @@ -use crate::{app::menu::Menu, export::ExportMessage, node::bitcoind::RpcAuthType}; +use crate::{app::menu::Menu, export::ImportExportMessage, node::bitcoind::RpcAuthType}; use liana::miniscript::bitcoin::{bip32::Fingerprint, OutPoint}; #[derive(Debug, Clone)] @@ -19,7 +19,7 @@ pub enum Message { SelectHardwareWallet(usize), CreateRbf(CreateRbfMessage), ShowQrCode(usize), - Export(ExportMessage), + ImportExport(ImportExportMessage), } #[derive(Debug, Clone)] diff --git a/liana-gui/src/app/view/transactions.rs b/liana-gui/src/app/view/transactions.rs index 1694deb1..e9cee700 100644 --- a/liana-gui/src/app/view/transactions.rs +++ b/liana-gui/src/app/view/transactions.rs @@ -25,7 +25,7 @@ use crate::{ }, }, daemon::model::{HistoryTransaction, Txid}, - export::ExportMessage, + export::ImportExportMessage, }; pub fn transactions_view<'a>( @@ -44,7 +44,10 @@ pub fn transactions_view<'a>( Row::new() .push(Container::new(h3("Transactions"))) .push(Space::with_width(Length::Fill)) - .push(button::secondary(None, "Export").on_press(ExportMessage::Open.into())), + .push( + button::secondary(None, "Export") + .on_press(ImportExportMessage::Open.into()), + ), ) .push( Column::new() diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index 5a3cd65f..8ec294a5 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -1,8 +1,9 @@ use std::{ collections::HashMap, fs::{self, File}, - io::Write, + io::{Read, Write}, path::PathBuf, + str::FromStr, sync::{ mpsc::{channel, Receiver, Sender}, Arc, Mutex, @@ -13,7 +14,7 @@ use std::{ use chrono::{DateTime, Duration, Utc}; use liana::{ descriptors::LianaDescriptor, - miniscript::bitcoin::{Amount, Txid}, + miniscript::bitcoin::{Amount, Psbt, Txid}, }; use tokio::{ task::{JoinError, JoinHandle}, @@ -33,26 +34,26 @@ use crate::{ macro_rules! send_error { ($sender:ident, $error:ident) => { - if let Err(e) = $sender.send(ExportProgress::Error(Error::$error)) { - tracing::error!("ExportState::start() fail to send msg: {}", e); + if let Err(e) = $sender.send(Progress::Error(Error::$error)) { + tracing::error!("Import/Export fail to send msg: {}", e); } }; ($sender:ident, $error:expr) => { - if let Err(e) = $sender.send(ExportProgress::Error($error)) { - tracing::error!("ExportState::start() fail to send msg: {}", e); + if let Err(e) = $sender.send(Progress::Error($error)) { + tracing::error!("Import/Export fail to send msg: {}", e); } }; } macro_rules! send_progress { ($sender:ident, $progress:ident) => { - if let Err(e) = $sender.send(ExportProgress::$progress) { - tracing::error!("ExportState::start() fail to send msg: {}", e); + if let Err(e) = $sender.send(Progress::$progress) { + tracing::error!("ImportExport fail to send msg: {}", e); } }; ($sender:ident, $progress:ident($val:expr)) => { - if let Err(e) = $sender.send(ExportProgress::$progress($val)) { - tracing::error!("ExportState::start() fail to send msg: {}", e); + if let Err(e) = $sender.send(Progress::$progress($val)) { + tracing::error!("ImportExport fail to send msg: {}", e); } }; } @@ -83,23 +84,23 @@ macro_rules! open_file { } #[derive(Debug, Clone)] -pub enum ExportMessage { +pub enum ImportExportMessage { Open, - ExportProgress(ExportProgress), + Progress(Progress), TimedOut, UserStop, Path(Option), Close, } -impl From for view::Message { - fn from(value: ExportMessage) -> Self { - Self::Export(value) +impl From for view::Message { + fn from(value: ImportExportMessage) -> Self { + Self::ImportExport(value) } } #[derive(Debug, PartialEq)] -pub enum ExportState { +pub enum ImportExportState { Init, ChoosePath, Path(PathBuf), @@ -121,13 +122,18 @@ pub enum Error { NoParentDir, Daemon(String), TxTimeMissing, + DaemonMissing, + ParsePsbt, + ParseDescriptor, } #[derive(Debug, PartialEq, Clone)] -pub enum ExportType { +pub enum ImportExportType { Transactions, - Psbt(String), + ExportPsbt(String), Descriptor(LianaDescriptor), + ImportPsbt, + ImportDescriptor, } impl From for Error { @@ -156,29 +162,31 @@ pub enum Status { } #[derive(Debug, Clone)] -pub enum ExportProgress { +pub enum Progress { Started(Arc>>), Progress(f32), Ended, Finished, Error(Error), None, + Psbt(Psbt), + Descriptor(LianaDescriptor), } pub struct Export { - pub receiver: Receiver, - pub sender: Option>, + pub receiver: Receiver, + pub sender: Option>, pub handle: Option>>>, - pub daemon: Arc, + pub daemon: Option>, pub path: Box, - pub export_type: ExportType, + pub export_type: ImportExportType, } impl Export { pub fn new( - daemon: Arc, + daemon: Option>, path: Box, - export_type: ExportType, + export_type: ImportExportType, ) -> Self { let (sender, receiver) = channel(); Export { @@ -192,15 +200,17 @@ impl Export { } pub async fn export_logic( - export_type: ExportType, - sender: Sender, - daemon: Arc, + export_type: ImportExportType, + sender: Sender, + daemon: Option>, path: PathBuf, ) { match export_type { - ExportType::Transactions => export_transactions(sender, daemon, path).await, - ExportType::Psbt(psbt) => export_psbt(sender, path, psbt), - ExportType::Descriptor(descriptor) => export_descriptor(sender, path, descriptor), + ImportExportType::Transactions => export_transactions(sender, daemon, path).await, + ImportExportType::ExportPsbt(psbt) => export_psbt(sender, path, psbt), + ImportExportType::Descriptor(descriptor) => export_descriptor(sender, path, descriptor), + ImportExportType::ImportPsbt => import_psbt(sender, path), + ImportExportType::ImportDescriptor => import_descriptor(sender, path), }; } @@ -238,10 +248,10 @@ impl Export { pub fn export_subscription( daemon: Arc, path: PathBuf, - export_type: ExportType, -) -> impl Stream { + export_type: ImportExportType, +) -> impl Stream { iced::stream::channel(100, move |mut output| async move { - let mut state = Export::new(daemon, Box::new(path), export_type); + let mut state = Export::new(Some(daemon), Box::new(path), export_type); loop { match state.state() { Status::Init => { @@ -269,7 +279,7 @@ pub fn export_subscription( let handle = match state.handle.take() { Some(h) => h, None => { - if let Err(e) = output.send(ExportProgress::Error(Error::HandleLost)).await { + if let Err(e) = output.send(Progress::Error(Error::HandleLost)).await { tracing::error!("export_subscription() fail to send message: {}", e); } continue; @@ -278,9 +288,9 @@ pub fn export_subscription( let msg = { let h = handle.lock().expect("should not fail"); if h.is_finished() { - Some(ExportProgress::Finished) + Some(Progress::Finished) } else if disconnected { - Some(ExportProgress::Error(Error::ChannelLost)) + Some(Progress::Error(Error::ChannelLost)) } else { None } @@ -299,11 +309,18 @@ pub fn export_subscription( } pub async fn export_transactions( - sender: Sender, - daemon: Arc, + sender: Sender, + daemon: Option>, path: PathBuf, ) { async move { + let daemon = match daemon { + Some(d) => d, + None => { + send_error!(sender, Error::DaemonMissing); + return; + } + }; let mut file = open_file!(path, sender); let header = "Date,Label,Value,Fee,Txid,Block\n".to_string(); @@ -456,11 +473,7 @@ pub async fn export_transactions( .await; } -pub fn export_descriptor( - sender: Sender, - path: PathBuf, - descriptor: LianaDescriptor, -) { +pub fn export_descriptor(sender: Sender, path: PathBuf, descriptor: LianaDescriptor) { let mut file = open_file!(path, sender); let descr_string = descriptor.to_string(); @@ -472,7 +485,7 @@ pub fn export_descriptor( send_progress!(sender, Ended); } -pub fn export_psbt(sender: Sender, path: PathBuf, psbt: String) { +pub fn export_psbt(sender: Sender, path: PathBuf, psbt: String) { let mut file = open_file!(path, sender); if let Err(e) = file.write_all(psbt.as_bytes()) { @@ -483,6 +496,48 @@ pub fn export_psbt(sender: Sender, path: PathBuf, psbt: String) send_progress!(sender, Ended); } +pub fn import_psbt(sender: Sender, path: PathBuf) { + let mut file = open_file!(path, sender); + + let mut psbt_str = String::new(); + if let Err(e) = file.read_to_string(&mut psbt_str) { + send_error!(sender, e.into()); + return; + } + + let psbt = match Psbt::from_str(&psbt_str) { + Ok(psbt) => psbt, + Err(_) => { + send_error!(sender, Error::ParsePsbt); + return; + } + }; + + send_progress!(sender, Progress(100.0)); + send_progress!(sender, Psbt(psbt)); +} + +pub fn import_descriptor(sender: Sender, path: PathBuf) { + let mut file = open_file!(path, sender); + + let mut descr_str = String::new(); + if let Err(e) = file.read_to_string(&mut descr_str) { + send_error!(sender, e.into()); + return; + } + + let descriptor = match LianaDescriptor::from_str(&descr_str) { + Ok(psbt) => psbt, + Err(_) => { + send_error!(sender, Error::ParseDescriptor); + return; + } + }; + + send_progress!(sender, Progress(100.0)); + send_progress!(sender, Descriptor(descriptor)); +} + pub async fn get_path(filename: String) -> Option { rfd::AsyncFileDialog::new() .set_title("Choose a location to export...") From 38eed1b8810976ef903063d7f74b39dd06affcfc Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Wed, 22 Jan 2025 10:22:53 +0100 Subject: [PATCH 06/44] lianad: add feature to dump labels in BIP-0329 format --- Cargo.lock | 809 +++++++++--------- liana-gui/src/daemon/client/mod.rs | 5 + liana-gui/src/daemon/embedded.rs | 6 + liana-gui/src/daemon/mod.rs | 2 + liana-gui/src/lianalite/client/backend/mod.rs | 6 + lianad/Cargo.toml | 1 + lianad/src/commands/mod.rs | 12 + lianad/src/database/mod.rs | 13 + lianad/src/database/sqlite/mod.rs | 12 + lianad/src/database/sqlite/schema.rs | 26 + lianad/src/jsonrpc/api.rs | 22 + lianad/src/lib.rs | 1 + lianad/src/testutils.rs | 4 + 13 files changed, 518 insertions(+), 401 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 059e7535..79b4137f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,7 +75,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", "zerocopy", @@ -98,9 +98,9 @@ checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" [[package]] name = "allocator-api2" -version = "0.2.18" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-activity" @@ -109,7 +109,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", - "bitflags 2.6.0", + "bitflags 2.8.0", "cc", "cesu8", "jni", @@ -146,9 +146,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.93" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] name = "approx" @@ -219,9 +219,9 @@ dependencies = [ [[package]] name = "async-broadcast" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ "event-listener", "event-listener-strategy", @@ -358,7 +358,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -387,13 +387,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.83" +version = "0.1.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -514,6 +514,17 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" +[[package]] +name = "bip329" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdacd3f71820072c8d76eb077a8177b9a001bf03723d797783f97e9feefc0515" +dependencies = [ + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "bip39" version = "2.1.0" @@ -557,13 +568,13 @@ dependencies = [ "bitcoin", "byteorder", "chrono", - "getrandom", + "getrandom 0.2.15", "hex", "hidapi", "noise-protocol", "noise-rust-crypto", "num-bigint", - "prost 0.13.3", + "prost 0.13.4", "prost-build", "semver", "serde", @@ -575,9 +586,9 @@ dependencies = [ [[package]] name = "bitcoin" -version = "0.32.4" +version = "0.32.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "788902099d47c8682efe6a7afb01c8d58b9794ba66c06affd81c3d6b560743eb" +checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026" dependencies = [ "base58ck", "base64 0.21.7", @@ -667,9 +678,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" [[package]] name = "blake2" @@ -719,9 +730,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "by_address" @@ -731,22 +742,22 @@ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" -version = "1.19.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec" +checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -757,9 +768,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" [[package]] name = "bzip2" @@ -788,7 +799,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "log", "polling", "rustix", @@ -810,9 +821,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.36" +version = "1.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baee610e9452a8f6f0a1b6194ec09ff9e2d85dea54432acdae41aa0761c95d70" +checksum = "755717a7de9ec452bf7f3f1a3099085deabd7f2962b861dae91ecd7a365903d2" dependencies = [ "jobserver", "libc", @@ -869,9 +880,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", @@ -1033,7 +1044,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom", + "getrandom 0.2.15", "once_cell", "tiny-keccak", ] @@ -1083,7 +1094,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "core-foundation 0.10.0", "core-graphics-types 0.2.0", "foreign-types", @@ -1107,7 +1118,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "core-foundation 0.10.0", "libc", ] @@ -1118,7 +1129,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59fd57d82eb4bfe7ffa9b1cec0c05e2fd378155b47f255a67983cb4afe0e80c2" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "fontdb 0.16.2", "log", "rangemap", @@ -1137,9 +1148,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.14" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -1155,9 +1166,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -1174,15 +1185,15 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" [[package]] name = "crypto-bigint" @@ -1250,7 +1261,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -1259,8 +1270,8 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e3d747f100290a1ca24b752186f61f6637e1deffe3bf6320de6fcb29510a307" dependencies = [ - "bitflags 2.6.0", - "libloading 0.8.5", + "bitflags 2.8.0", + "libloading 0.8.6", "winapi", ] @@ -1288,7 +1299,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -1358,7 +1369,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -1367,7 +1378,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.8.5", + "libloading 0.8.6", ] [[package]] @@ -1403,7 +1414,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98888c4bbd601524c11a7ed63f814b8825f420514f78e96f752c437ae9cbb5d1" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "bytemuck", "drm-ffi", "drm-fourcc", @@ -1466,7 +1477,7 @@ dependencies = [ "byteorder", "libc", "log", - "rustls 0.23.21", + "rustls 0.23.22", "serde", "serde_json", "webpki-roots", @@ -1518,9 +1529,9 @@ checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" [[package]] name = "enumflags2" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" +checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147" dependencies = [ "enumflags2_derive", "serde", @@ -1528,13 +1539,13 @@ dependencies = [ [[package]] name = "enumflags2_derive" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" +checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -1545,12 +1556,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1561,9 +1572,9 @@ checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" [[package]] name = "etagere" -version = "0.2.13" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e2f1e3be19fb10f549be8c1bf013e8675b4066c445e36eb76d2ebb2f54ee495" +checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" dependencies = [ "euclid", "svg_fmt", @@ -1580,9 +1591,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.3.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" dependencies = [ "concurrent-queue", "parking", @@ -1634,15 +1645,15 @@ checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" [[package]] name = "fastrand" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fdeflate" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c6f4c64c1d33a3111c4466f7365ebdcc37c5bd1ea0d62aae2e3d722aacbedb" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" dependencies = [ "simd-adler32", ] @@ -1692,9 +1703,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.34" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "miniz_oxide", @@ -1782,7 +1793,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -1851,9 +1862,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" dependencies = [ "fastrand", "futures-core", @@ -1870,7 +1881,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -1932,7 +1943,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", ] [[package]] @@ -2005,7 +2028,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "gpu-alloc-types", ] @@ -2015,7 +2038,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", ] [[package]] @@ -2037,7 +2060,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "gpu-descriptor-types", "hashbrown 0.14.5", ] @@ -2048,7 +2071,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bf0b36e6f090b7e1d8a4b49c0cb81c1f8376f72198c65dd3ad9ff3556b8b78c" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", ] [[package]] @@ -2125,9 +2148,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "hashlink" @@ -2144,10 +2167,10 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "com", "libc", - "libloading 0.8.5", + "libloading 0.8.6", "thiserror", "widestring", "winapi", @@ -2228,11 +2251,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2259,9 +2282,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.5" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" [[package]] name = "httpdate" @@ -2271,9 +2294,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.31" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", @@ -2351,14 +2374,14 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0013a238275494641bf8f1732a23a808196540dc67b22ff97099c044ae4c8a1c" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "bytes", "glam", "log", "num-traits", "once_cell", "palette", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "smol_str", "thiserror", "web-time", @@ -2373,7 +2396,7 @@ dependencies = [ "futures", "iced_core", "log", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "tokio", "wasm-bindgen-futures", "wasm-timer", @@ -2388,7 +2411,7 @@ dependencies = [ "cosmic-text", "etagere", "lru", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "wgpu", ] @@ -2398,7 +2421,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba25a18cfa6d5cc160aca7e1b34f73ccdff21680fa8702168c09739767b6c66f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "bytemuck", "cosmic-text", "half 2.4.1", @@ -2410,7 +2433,7 @@ dependencies = [ "lyon_path", "once_cell", "raw-window-handle", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "thiserror", "unicode-segmentation", ] @@ -2453,7 +2476,7 @@ dependencies = [ "kurbo 0.10.4", "log", "resvg", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "softbuffer", "tiny-skia", ] @@ -2464,7 +2487,7 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15708887133671d2bcc6c1d01d1f176f43a64d6cdc3b2bf893396c3ee498295f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "bytemuck", "futures", "glam", @@ -2475,7 +2498,7 @@ dependencies = [ "lyon", "once_cell", "resvg", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "thiserror", "wgpu", ] @@ -2492,7 +2515,7 @@ dependencies = [ "once_cell", "ouroboros", "qrcode", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "thiserror", "unicode-segmentation", ] @@ -2507,7 +2530,7 @@ dependencies = [ "iced_graphics", "iced_runtime", "log", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "thiserror", "tracing", "wasm-bindgen-futures", @@ -2632,7 +2655,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -2682,12 +2705,12 @@ checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", - "hashbrown 0.15.1", + "hashbrown 0.15.2", ] [[package]] @@ -2720,9 +2743,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.10.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "itertools" @@ -2733,15 +2756,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -2753,9 +2767,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jni" @@ -2861,7 +2875,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" dependencies = [ "libc", - "libloading 0.8.5", + "libloading 0.8.6", "pkg-config", ] @@ -2957,7 +2971,7 @@ version = "9.0.0" dependencies = [ "bdk_coin_select", "bip39", - "getrandom", + "getrandom 0.2.15", "log", "miniscript", "rdrand", @@ -3026,6 +3040,7 @@ version = "9.0.0" dependencies = [ "backtrace", "bdk_electrum", + "bip329", "dirs 5.0.1", "fern", "jsonrpc 0.17.0", @@ -3040,19 +3055,18 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.162" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libfuzzer-sys" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" dependencies = [ "arbitrary", "cc", - "once_cell", ] [[package]] @@ -3067,9 +3081,9 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", "windows-targets 0.52.6", @@ -3087,9 +3101,9 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "libc", - "redox_syscall 0.5.7", + "redox_syscall 0.5.8", ] [[package]] @@ -3125,9 +3139,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" @@ -3137,9 +3151,9 @@ checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7" [[package]] name = "litemap" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "lock_api" @@ -3153,9 +3167,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "lru" @@ -3175,9 +3189,9 @@ dependencies = [ [[package]] name = "lyon_algorithms" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3bca95f9a4955b3e4a821fbbcd5edfbd9be2a9a50bb5758173e5358bfb4c623" +checksum = "f13c9be19d257c7d37e70608ed858e8eab4b2afcea2e3c9a622e892acbf43c08" dependencies = [ "lyon_path", "num-traits", @@ -3248,15 +3262,6 @@ dependencies = [ "libc", ] -[[package]] -name = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] - [[package]] name = "memoffset" version = "0.9.1" @@ -3272,7 +3277,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c43f73953f8cbe511f021b58f18c3ce1c3d1ae13fe953293e13345bf83217f25" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block", "core-graphics-types 0.1.3", "foreign-types", @@ -3300,9 +3305,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" dependencies = [ "adler2", "simd-adler32", @@ -3310,9 +3315,9 @@ dependencies = [ [[package]] name = "minreq" -version = "2.12.0" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763d142cdff44aaadd9268bebddb156ef6c65a0e13486bb81673cf2d8739f9b0" +checksum = "da0c420feb01b9fb5061f8c8f452534361dd783756dcf38ec45191ce55e7a161" dependencies = [ "log", "serde", @@ -3321,37 +3326,25 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.11" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "log", - "wasi", - "windows-sys 0.48.0", -] - -[[package]] -name = "mio" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" -dependencies = [ - "hermit-abi 0.3.9", - "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] [[package]] name = "mio-serial" -version = "5.0.5" +version = "5.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20a4c60ca5c9c0e114b3bd66ff4aa5f9b2b175442be51ca6c4365d687a97a2ac" +checksum = "029e1f407e261176a983a6599c084efd322d9301028055c87174beac71397ba3" dependencies = [ "log", - "mio 0.8.11", - "nix 0.26.4", + "mio", + "nix 0.29.0", "serialport", "winapi", ] @@ -3375,7 +3368,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50e3524642f53d9af419ab5e8dd29d3ba155708267667c2f3f06c88c9e130843" dependencies = [ "bit-set", - "bitflags 2.6.0", + "bitflags 2.8.0", "codespan-reporting", "hexf-parse", "indexmap", @@ -3394,7 +3387,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "jni-sys", "log", "ndk-sys 0.6.0+11769913", @@ -3436,8 +3429,6 @@ dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", - "memoffset 0.7.1", - "pin-utils", ] [[package]] @@ -3446,11 +3437,11 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cfg-if", "cfg_aliases 0.2.1", "libc", - "memoffset 0.9.1", + "memoffset", ] [[package]] @@ -3550,7 +3541,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -3585,7 +3576,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "libc", "objc2", @@ -3601,7 +3592,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "objc2", "objc2-core-location", @@ -3625,7 +3616,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "objc2", "objc2-foundation", @@ -3657,9 +3648,9 @@ dependencies = [ [[package]] name = "objc2-encode" -version = "4.0.3" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-foundation" @@ -3667,7 +3658,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "dispatch", "libc", @@ -3692,7 +3683,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "objc2", "objc2-foundation", @@ -3704,7 +3695,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "objc2", "objc2-foundation", @@ -3727,7 +3718,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "objc2", "objc2-cloud-kit", @@ -3759,7 +3750,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "objc2", "objc2-core-location", @@ -3777,9 +3768,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.5" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] @@ -3833,9 +3824,9 @@ dependencies = [ [[package]] name = "ouroboros" -version = "0.18.4" +version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "944fa20996a25aded6b4795c6d63f10014a7a83f8be9828a11860b08c5fc4a67" +checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" dependencies = [ "aliasable", "ouroboros_macro", @@ -3844,16 +3835,15 @@ dependencies = [ [[package]] name = "ouroboros_macro" -version = "0.18.4" +version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39b0deead1528fd0e5947a8546a9642a9777c25f6e1e26f34c97b204bbb465bd" +checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" dependencies = [ "heck", - "itertools 0.12.1", "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -3868,7 +3858,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4" dependencies = [ - "ttf-parser 0.25.0", + "ttf-parser 0.25.1", ] [[package]] @@ -3892,7 +3882,7 @@ dependencies = [ "by_address", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -3944,7 +3934,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.7", + "redox_syscall 0.5.8", "smallvec", "windows-targets 0.52.6", ] @@ -3973,9 +3963,9 @@ dependencies = [ [[package]] name = "phf" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ "phf_macros", "phf_shared", @@ -3983,9 +3973,9 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", "rand", @@ -3993,24 +3983,24 @@ dependencies = [ [[package]] name = "phf_macros" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ "phf_generator", "phf_shared", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] name = "phf_shared" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher 0.3.11", + "siphasher", ] [[package]] @@ -4021,29 +4011,29 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "pin-project" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" +checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" +checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -4080,9 +4070,9 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "png" -version = "0.17.14" +version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" dependencies = [ "bitflags 1.3.2", "crc32fast", @@ -4108,9 +4098,9 @@ dependencies = [ [[package]] name = "pollster" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" [[package]] name = "poly1305" @@ -4171,9 +4161,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] @@ -4186,7 +4176,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", "version_check", "yansi", ] @@ -4209,12 +4199,12 @@ dependencies = [ [[package]] name = "prost" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f" +checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec" dependencies = [ "bytes", - "prost-derive 0.13.3", + "prost-derive 0.13.4", ] [[package]] @@ -4254,15 +4244,15 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" +checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" dependencies = [ "anyhow", "itertools 0.13.0", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -4291,18 +4281,18 @@ checksum = "166f136dfdb199f98186f3649cf7a0536534a61417a1a30221b492b4fb60ce3f" [[package]] name = "quick-xml" -version = "0.36.2" +version = "0.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -4334,14 +4324,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", ] [[package]] name = "range-alloc" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8a99fddc9f0ba0a85884b8d14e3592853e787d581ca1816c91349b10e4eeab" +checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" [[package]] name = "rangemap" @@ -4414,11 +4404,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", ] [[package]] @@ -4427,7 +4417,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom", + "getrandom 0.2.15", "libredox", "thiserror", ] @@ -4446,9 +4436,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -4538,12 +4528,14 @@ dependencies = [ [[package]] name = "rfd" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f6f80a9b882647d9014673ca9925d30ffc9750f2eed2b4490e189eaebd01e8" +checksum = "6a24763657bff09769a8ccf12c8b8a50416fb035fe199263b4c5071e4e3f006f" dependencies = [ "ashpd", "block2", + "core-foundation 0.10.0", + "core-foundation-sys", "js-sys", "log", "objc2", @@ -4555,7 +4547,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -4575,7 +4567,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.15", "libc", "spin", "untrusted", @@ -4594,7 +4586,7 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a78046161564f5e7cd9008aff3b2990b3850dc8e0349119b98e8f251e099f24d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -4626,9 +4618,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustc_version" @@ -4641,15 +4633,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.39" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "errno", "libc", - "linux-raw-sys 0.4.14", - "windows-sys 0.52.0", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", ] [[package]] @@ -4666,9 +4658,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.21" +version = "0.23.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" +checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7" dependencies = [ "log", "once_cell", @@ -4690,9 +4682,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" [[package]] name = "rustls-webpki" @@ -4727,7 +4719,7 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "bytemuck", "libm", "smallvec", @@ -4740,9 +4732,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" [[package]] name = "same-file" @@ -4824,21 +4816,21 @@ dependencies = [ [[package]] name = "self_cell" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" +checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe" [[package]] name = "semver" -version = "1.0.23" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" [[package]] name = "serde" -version = "1.0.214" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] @@ -4864,20 +4856,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.214" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" dependencies = [ "itoa", "memchr", @@ -4893,7 +4885,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -4910,11 +4902,11 @@ dependencies = [ [[package]] name = "serialport" -version = "4.6.0" +version = "4.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7331eefcaafaa382c0df95bcd84068f0b3e3c215c300750dde2316e9b8806ed5" +checksum = "5ecfc4858c2266c7695d8b8460bbd612fa81bd2e250f5f0dd16195e4b4f8b3d8" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cfg-if", "core-foundation 0.10.0", "core-foundation-sys", @@ -4980,19 +4972,13 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "simplecss" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" dependencies = [ "log", ] -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - [[package]] name = "siphasher" version = "1.0.1" @@ -5039,7 +5025,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "calloop", "calloop-wayland-source", "cursor-icon", @@ -5102,9 +5088,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", @@ -5130,7 +5116,7 @@ dependencies = [ "objc2-foundation", "objc2-quartz-core", "raw-window-handle", - "redox_syscall 0.5.7", + "redox_syscall 0.5.8", "rustix", "tiny-xlib", "wasm-bindgen", @@ -5154,7 +5140,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", ] [[package]] @@ -5207,7 +5193,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" dependencies = [ "kurbo 0.11.1", - "siphasher 1.0.1", + "siphasher", ] [[package]] @@ -5234,9 +5220,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.87" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", @@ -5257,7 +5243,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -5302,12 +5288,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.13.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" dependencies = [ "cfg-if", "fastrand", + "getrandom 0.3.1", "once_cell", "rustix", "windows-sys 0.59.0", @@ -5324,22 +5311,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -5400,13 +5387,13 @@ dependencies = [ [[package]] name = "tiny-xlib" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d52f22673960ad13af14ff4025997312def1223bfa7c8e4949d099e6b3d5d1c" +checksum = "0324504befd01cab6e0c994f34b2ffa257849ee019d3fb3b64fb2c858887d89e" dependencies = [ "as-raw-xcb-connection", "ctor-lite", - "libloading 0.8.5", + "libloading 0.8.6", "pkg-config", "tracing", ] @@ -5423,9 +5410,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" dependencies = [ "tinyvec_macros", ] @@ -5438,14 +5425,14 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.41.1" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "bytes", "libc", - "mio 1.0.2", + "mio", "pin-project-lite", "signal-hook-registry", "socket2", @@ -5455,13 +5442,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -5476,22 +5463,23 @@ dependencies = [ [[package]] name = "tokio-serial" -version = "5.4.4" +version = "5.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa6e2e4cf0520a99c5f87d5abb24172b5bd220de57c3181baaaa5440540c64aa" +checksum = "aa1d5427f11ba7c5e6384521cfd76f2d64572ff29f3f4f7aa0f496282923fdc8" dependencies = [ "cfg-if", "futures", "log", "mio-serial", + "serialport", "tokio", ] [[package]] name = "tokio-util" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", @@ -5517,9 +5505,9 @@ checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" dependencies = [ "indexmap", "toml_datetime", @@ -5534,9 +5522,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -5545,20 +5533,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", @@ -5577,9 +5565,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "nu-ansi-term", "sharded-slab", @@ -5609,9 +5597,9 @@ checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" [[package]] name = "ttf-parser" -version = "0.25.0" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5902c5d130972a0000f60860bfbf46f7ca3db5391eddfedd1b8728bd9dc96c0e" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" [[package]] name = "typenum" @@ -5625,7 +5613,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" dependencies = [ - "memoffset 0.9.1", + "memoffset", "tempfile", "winapi", ] @@ -5641,9 +5629,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-bidi-mirroring" @@ -5659,9 +5647,9 @@ checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" [[package]] name = "unicode-linebreak" @@ -5732,9 +5720,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", @@ -5765,7 +5753,7 @@ dependencies = [ "roxmltree", "rustybuzz", "simplecss", - "siphasher 1.0.1", + "siphasher", "strict-num", "svgtypes", "tiny-skia-path", @@ -5789,9 +5777,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "valuable" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "vcpkg" @@ -5830,6 +5818,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -5852,18 +5849,19 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.45" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] @@ -5886,7 +5884,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5930,9 +5928,9 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6" +checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" dependencies = [ "cc", "downcast-rs", @@ -5944,11 +5942,11 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280" +checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "rustix", "wayland-backend", "wayland-scanner", @@ -5960,16 +5958,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cursor-icon", "wayland-backend", ] [[package]] name = "wayland-cursor" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b08bc3aafdb0035e7fe0fdf17ba0c09c268732707dca4ae098f60cb28c9e4c" +checksum = "a93029cbb6650748881a00e4922b076092a6a08c11e7fbdb923f064b23968c5d" dependencies = [ "rustix", "wayland-client", @@ -5978,11 +5976,11 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.32.5" +version = "0.32.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd0ade57c4e6e9a8952741325c30bf82f4246885dca8bf561898b86d0c1f58e" +checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -5990,11 +5988,11 @@ dependencies = [ [[package]] name = "wayland-protocols-plasma" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b31cab548ee68c7eb155517f2212049dc151f7cd7910c2b66abfd31c3ee12bd" +checksum = "7ccaacc76703fefd6763022ac565b590fcade92202492381c95b2edfdf7d46b3" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -6003,11 +6001,11 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "782e12f6cd923c3c316130d56205ebab53f55d6666b7faddfad36cecaeeb4022" +checksum = "248a02e6f595aad796561fa82d25601bd2c8c3b145b1c7453fc8f94c1a58f8b2" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -6016,9 +6014,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.5" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3" +checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" dependencies = [ "proc-macro2", "quick-xml", @@ -6027,9 +6025,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.5" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09" +checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" dependencies = [ "dlib", "log", @@ -6102,7 +6100,7 @@ checksum = "28b94525fc99ba9e5c9a9e24764f2bc29bad0911a7446c12f446a8277369bf3a" dependencies = [ "arrayvec", "bit-vec", - "bitflags 2.6.0", + "bitflags 2.8.0", "cfg_aliases 0.1.1", "codespan-reporting", "indexmap", @@ -6130,7 +6128,7 @@ dependencies = [ "arrayvec", "ash", "bit-set", - "bitflags 2.6.0", + "bitflags 2.8.0", "block", "cfg_aliases 0.1.1", "core-graphics-types 0.1.3", @@ -6144,7 +6142,7 @@ dependencies = [ "js-sys", "khronos-egl", "libc", - "libloading 0.8.5", + "libloading 0.8.6", "log", "metal", "naga", @@ -6171,7 +6169,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b671ff9fb03f78b46ff176494ee1ebe7d603393f42664be55b64dc8d53969805" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "js-sys", "web-sys", ] @@ -6481,7 +6479,7 @@ dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "bytemuck", "calloop", @@ -6526,9 +6524,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.20" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f" dependencies = [ "memchr", ] @@ -6543,6 +6541,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.8.0", +] + [[package]] name = "write16" version = "1.0.0" @@ -6575,7 +6582,7 @@ dependencies = [ "as-raw-xcb-connection", "gethostname", "libc", - "libloading 0.8.5", + "libloading 0.8.6", "once_cell", "rustix", "x11rb-protocol", @@ -6620,7 +6627,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "dlib", "log", "once_cell", @@ -6635,9 +6642,9 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xml-rs" -version = "0.8.23" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af310deaae937e48a26602b730250b4949e125f468f11e6990be3e5304ddd96f" +checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" [[package]] name = "xmlwriter" @@ -6659,9 +6666,9 @@ checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1" [[package]] name = "yoke" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", @@ -6671,21 +6678,21 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", "synstructure", ] [[package]] name = "zbus" -version = "5.2.0" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb67eadba43784b6fb14857eba0d8fc518686d3ee537066eb6086dc318e2c8a1" +checksum = "cbddd8b6cb25d5d8ec1b23277b45299a98bfb220f1761ca11e186d5c702507f8" dependencies = [ "async-broadcast", "async-executor", @@ -6719,14 +6726,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.2.0" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d49ebc960ceb660f2abe40a5904da975de6986f2af0d7884b39eec6528c57" +checksum = "dac404d48b4e9cf193c8b49589f3280ceca5ff63519e7e64f55b4cf9c47ce146" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", "zbus_names", "zvariant", "zvariant_utils", @@ -6734,9 +6741,9 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.1.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "856b7a38811f71846fd47856ceee8bccaec8399ff53fb370247e66081ace647b" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" dependencies = [ "serde", "static_assertions", @@ -6768,27 +6775,27 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] name = "zerofrom" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", "synstructure", ] @@ -6809,7 +6816,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -6831,7 +6838,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -6858,9 +6865,9 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.1.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1200ee6ac32f1e5a312e455a949a4794855515d34f9909f4a3e082d14e1a56f" +checksum = "31c951c21879c6e1d46ac5adfc34f698fefb465d498cf4ac87545849bd71bb5a" dependencies = [ "endi", "enumflags2", @@ -6874,27 +6881,27 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.1.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "687e3b97fae6c9104fbbd36c73d27d149abf04fb874e2efbd84838763daa8916" +checksum = "9eeb539471af098d9e63faf428c71ac4cd4efe0b5baa3c8a6b991c5f2543b70e" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "3.0.2" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20d1d011a38f12360e5fcccceeff5e2c42a8eb7f27f0dcba97a0862ede05c9c6" +checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" dependencies = [ "proc-macro2", "quote", "serde", "static_assertions", - "syn 2.0.87", + "syn 2.0.98", "winnow", ] diff --git a/liana-gui/src/daemon/client/mod.rs b/liana-gui/src/daemon/client/mod.rs index ec6abe03..fc7708dd 100644 --- a/liana-gui/src/daemon/client/mod.rs +++ b/liana-gui/src/daemon/client/mod.rs @@ -4,6 +4,7 @@ use std::iter::FromIterator; use std::path::Path; use async_trait::async_trait; +use lianad::bip329::Labels; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -205,6 +206,10 @@ impl Daemon for Lianad { let _res: serde_json::value::Value = self.call("updatelabels", Some(vec![labels]))?; Ok(()) } + + async fn get_labels_bip329(&self, _offset: u32, _limit: u32) -> Result { + todo!() + } } #[derive(Debug, Clone, Deserialize, Serialize)] diff --git a/liana-gui/src/daemon/embedded.rs b/liana-gui/src/daemon/embedded.rs index 06318b03..4cafd5cc 100644 --- a/liana-gui/src/daemon/embedded.rs +++ b/liana-gui/src/daemon/embedded.rs @@ -1,3 +1,4 @@ +use lianad::bip329::Labels; use std::collections::{HashMap, HashSet}; use std::path::Path; use tokio::sync::Mutex; @@ -227,4 +228,9 @@ impl Daemon for EmbeddedDaemon { }) .await } + + async fn get_labels_bip329(&self, offset: u32, limit: u32) -> Result { + self.command(|daemon| Ok(daemon.get_labels_bip329(offset, limit).labels)) + .await + } } diff --git a/liana-gui/src/daemon/mod.rs b/liana-gui/src/daemon/mod.rs index 3f0211fc..f8779267 100644 --- a/liana-gui/src/daemon/mod.rs +++ b/liana-gui/src/daemon/mod.rs @@ -14,6 +14,7 @@ use async_trait::async_trait; use liana::miniscript::bitcoin::{ address, bip32::Fingerprint, psbt::Psbt, secp256k1, Address, Network, OutPoint, Txid, }; +use lianad::bip329::Labels; use lianad::{ commands::{CoinStatus, LabelItem, TransactionInfo}, config::Config, @@ -125,6 +126,7 @@ pub trait Daemon: Debug { &self, labels: &HashMap>, ) -> Result<(), DaemonError>; + async fn get_labels_bip329(&self, offset: u32, limit: u32) -> Result; async fn send_wallet_invitation(&self, _email: &str) -> Result<(), DaemonError> { Ok(()) } diff --git a/liana-gui/src/lianalite/client/backend/mod.rs b/liana-gui/src/lianalite/client/backend/mod.rs index 7b8eca2a..0e75184f 100644 --- a/liana-gui/src/lianalite/client/backend/mod.rs +++ b/liana-gui/src/lianalite/client/backend/mod.rs @@ -13,6 +13,7 @@ use liana::{ miniscript::bitcoin::{address, psbt::Psbt, Address, Network, OutPoint, Txid}, }; use lianad::{ + bip329::Labels, commands::{CoinStatus, GetInfoDescriptors, LCSpendInfo, LabelItem}, config::Config, }; @@ -1115,6 +1116,11 @@ impl Daemon for BackendWalletClient { Ok(()) } + + async fn get_labels_bip329(&self, _offset: u32, _limit: u32) -> Result { + // TODO: add an endpoint on backend + todo!() + } } fn history_tx_from_api(value: api::Transaction, network: Network) -> HistoryTransaction { diff --git a/lianad/Cargo.toml b/lianad/Cargo.toml index bbe83b2e..6d0d3727 100644 --- a/lianad/Cargo.toml +++ b/lianad/Cargo.toml @@ -55,3 +55,4 @@ rusqlite = { version = "0.30", features = ["bundled", "unlock_notify"] } # To talk to bitcoind jsonrpc = { version = "0.17", features = ["minreq_http"], default-features = false } +bip329 = {version = "0.1.1", default-features = false } diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index 6f04f1ed..d197f437 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -687,6 +687,13 @@ impl DaemonControl { } } + pub fn get_labels_bip329(&self, offset: u32, limit: u32) -> GetLabelsBip329Result { + let mut db_conn = self.db.connection(); + GetLabelsBip329Result { + labels: db_conn.get_labels_bip329(offset, limit), + } + } + pub fn list_spend( &self, txids: Option>, @@ -1184,6 +1191,11 @@ pub struct GetLabelsResult { pub labels: HashMap, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetLabelsBip329Result { + pub labels: crate::bip329::Labels, +} + #[derive(Debug, Clone, Serialize, PartialEq, Eq)] pub struct AddressInfo { index: u32, diff --git a/lianad/src/database/mod.rs b/lianad/src/database/mod.rs index c8f13ca5..d88c598b 100644 --- a/lianad/src/database/mod.rs +++ b/lianad/src/database/mod.rs @@ -20,6 +20,7 @@ use std::{ sync, }; +use bip329::Labels; use miniscript::bitcoin::{self, bip32, psbt::Psbt, secp256k1}; /// Information about the wallet. @@ -190,6 +191,9 @@ pub trait DatabaseConnection { &mut self, txids: &[bitcoin::Txid], ) -> Vec<(bitcoin::Transaction, Option, Option)>; + + /// Dump all labels + fn get_labels_bip329(&mut self, offset: u32, limit: u32) -> Labels; } impl DatabaseConnection for SqliteConn { @@ -367,6 +371,15 @@ impl DatabaseConnection for SqliteConn { HashMap::from_iter(labels.into_iter().map(|label| (label.item, label.value))) } + fn get_labels_bip329(&mut self, offset: u32, limit: u32) -> Labels { + let labels = self + .labels_bip329(offset, limit) + .into_iter() + .map(|l| l.into()) + .collect(); + Labels::new(labels) + } + fn rollback_tip(&mut self, new_tip: &BlockChainTip) { self.rollback_tip(new_tip) } diff --git a/lianad/src/database/sqlite/mod.rs b/lianad/src/database/sqlite/mod.rs index 5ec1462e..5f6876ee 100644 --- a/lianad/src/database/sqlite/mod.rs +++ b/lianad/src/database/sqlite/mod.rs @@ -678,6 +678,18 @@ impl SqliteConn { .expect("Db must not fail") } + pub fn labels_bip329(&mut self, offset: u32, limit: u32) -> Vec { + db_query( + &mut self.conn, + "SELECT * FROM labels \ + ORDER BY id \ + LIMIT ?1 OFFSET ?2", + rusqlite::params![limit, offset], + |row| row.try_into(), + ) + .expect("Db must not fail") + } + /// Retrieves a limited and ordered list of transactions ids that happened during the given /// range. pub fn db_list_txids(&mut self, start: u32, end: u32, limit: u64) -> Vec { diff --git a/lianad/src/database/sqlite/schema.rs b/lianad/src/database/sqlite/schema.rs index 1e6a7610..7b71990b 100644 --- a/lianad/src/database/sqlite/schema.rs +++ b/lianad/src/database/sqlite/schema.rs @@ -1,3 +1,4 @@ +use bip329::Label; use liana::descriptors::LianaDescriptor; use std::{convert::TryFrom, str::FromStr}; @@ -366,6 +367,31 @@ impl From for DbLabelledKind { } } +impl From for Label { + fn from(value: DbLabel) -> Self { + let ref_ = value.item; + let label = if value.value.is_empty() { + None + } else { + Some(value.value) + }; + match value.item_kind { + DbLabelledKind::Address => Label::Address(bip329::AddressRecord { ref_, label }), + DbLabelledKind::OutPoint => Label::Output(bip329::OutputRecord { + ref_, + label, + spendable: None, + }), + DbLabelledKind::Txid => Label::Transaction(bip329::TransactionRecord { + ref_, + label, + // FIXME: "Optional key origin information referencing the wallet associated with the label" + origin: None, + }), + } + } +} + impl TryFrom<&rusqlite::Row<'_>> for DbLabel { type Error = rusqlite::Error; diff --git a/lianad/src/jsonrpc/api.rs b/lianad/src/jsonrpc/api.rs index d27a7f1d..fe087a93 100644 --- a/lianad/src/jsonrpc/api.rs +++ b/lianad/src/jsonrpc/api.rs @@ -364,6 +364,22 @@ fn get_labels(control: &DaemonControl, params: Params) -> Result Result { + let offset: u32 = params + .get(0, "offset") + .ok_or_else(|| Error::invalid_params("Missing 'offset' parameter."))? + .as_u64() + .and_then(|t| t.try_into().ok()) + .ok_or_else(|| Error::invalid_params("Invalid 'offset' parameter."))?; + let limit: u32 = params + .get(1, "limit") + .ok_or_else(|| Error::invalid_params("Missing 'limit' parameter."))? + .as_u64() + .and_then(|t| t.try_into().ok()) + .ok_or_else(|| Error::invalid_params("Invalid 'limit' parameter."))?; + Ok(serde_json::json!(control.get_labels_bip329(offset, limit))) +} + /// Handle an incoming JSONRPC2 request. pub fn handle_request(control: &mut DaemonControl, req: Request) -> Result { let result = match req.method.as_str() { @@ -451,6 +467,12 @@ pub fn handle_request(control: &mut DaemonControl, req: Request) -> Result { + let params = req + .params + .ok_or_else(|| Error::invalid_params("Missing 'offset' and 'limit' parameters."))?; + get_labels_bip329(control, params)? + } _ => { return Err(Error::method_not_found()); } diff --git a/lianad/src/lib.rs b/lianad/src/lib.rs index 7ad1333e..ea40ac46 100644 --- a/lianad/src/lib.rs +++ b/lianad/src/lib.rs @@ -7,6 +7,7 @@ mod jsonrpc; mod testutils; pub use bdk_electrum::electrum_client; +pub use bip329; use bitcoin::electrum; pub use miniscript; diff --git a/lianad/src/testutils.rs b/lianad/src/testutils.rs index aacf64a3..dd05f716 100644 --- a/lianad/src/testutils.rs +++ b/lianad/src/testutils.rs @@ -523,6 +523,10 @@ impl DatabaseConnection for DummyDatabase { } wallet_txs } + + fn get_labels_bip329(&mut self, _offset: u32, _limit: u32) -> bip329::Labels { + todo!() + } } pub struct DummyLiana { From 86313f528221ddb7d766b3f59d4f255b4f49eedf Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Mon, 10 Mar 2025 16:08:09 +0100 Subject: [PATCH 07/44] export: implement export labels using BIP-0329 --- liana-gui/src/app/state/export.rs | 3 +- liana-gui/src/daemon/client/mod.rs | 7 ++- liana-gui/src/export.rs | 60 +++++++++++++++++++ liana-gui/src/lianalite/client/backend/api.rs | 5 ++ liana-gui/src/lianalite/client/backend/mod.rs | 19 +++++- 5 files changed, 88 insertions(+), 6 deletions(-) diff --git a/liana-gui/src/app/state/export.rs b/liana-gui/src/app/state/export.rs index e133f002..163c5eea 100644 --- a/liana-gui/src/app/state/export.rs +++ b/liana-gui/src/app/state/export.rs @@ -40,9 +40,9 @@ impl ExportModal { } pub fn default_filename(&self) -> String { + let date = chrono::Local::now().format("%Y-%m-%dT%H-%M-%S"); match &self.import_export_type { ImportExportType::Transactions => { - let date = chrono::Local::now().format("%Y-%m-%dT%H-%M-%S"); format!("liana-txs-{date}.csv") } ImportExportType::ExportPsbt(_) => "psbt.psbt".into(), @@ -57,6 +57,7 @@ impl ExportModal { } ImportExportType::ImportPsbt => "psbt.psbt".into(), ImportExportType::ImportDescriptor => "descriptor.descriptor".into(), + ImportExportType::ExportLabels => format!("liana-labels-{date}.csv"), } } diff --git a/liana-gui/src/daemon/client/mod.rs b/liana-gui/src/daemon/client/mod.rs index fc7708dd..80ba9176 100644 --- a/liana-gui/src/daemon/client/mod.rs +++ b/liana-gui/src/daemon/client/mod.rs @@ -5,6 +5,7 @@ use std::path::Path; use async_trait::async_trait; use lianad::bip329::Labels; +use lianad::commands::GetLabelsBip329Result; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -207,8 +208,10 @@ impl Daemon for Lianad { Ok(()) } - async fn get_labels_bip329(&self, _offset: u32, _limit: u32) -> Result { - todo!() + async fn get_labels_bip329(&self, offset: u32, limit: u32) -> Result { + let res: GetLabelsBip329Result = + self.call("getlabelsbip329", Some(vec![json!(offset), json!(limit)]))?; + Ok(res.labels) } } diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index 8ec294a5..57baf7cc 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -16,6 +16,7 @@ use liana::{ descriptors::LianaDescriptor, miniscript::bitcoin::{Amount, Psbt, Txid}, }; +use lianad::bip329::{error::ExportError, Labels}; use tokio::{ task::{JoinError, JoinHandle}, time::sleep, @@ -32,6 +33,8 @@ use crate::{ lianalite::client::backend::api::DEFAULT_LIMIT, }; +const DUMP_LABELS_LIMIT: u32 = 100; + macro_rules! send_error { ($sender:ident, $error:ident) => { if let Err(e) = $sender.send(Progress::Error(Error::$error)) { @@ -125,6 +128,7 @@ pub enum Error { DaemonMissing, ParsePsbt, ParseDescriptor, + Bip329Export(String), } #[derive(Debug, PartialEq, Clone)] @@ -132,6 +136,7 @@ pub enum ImportExportType { Transactions, ExportPsbt(String), Descriptor(LianaDescriptor), + ExportLabels, ImportPsbt, ImportDescriptor, } @@ -154,6 +159,12 @@ impl From for Error { } } +impl From for Error { + fn from(value: ExportError) -> Self { + Error::Bip329Export(format!("{:?}", value)) + } +} + #[derive(Debug)] pub enum Status { Init, @@ -209,6 +220,7 @@ impl Export { ImportExportType::Transactions => export_transactions(sender, daemon, path).await, ImportExportType::ExportPsbt(psbt) => export_psbt(sender, path, psbt), ImportExportType::Descriptor(descriptor) => export_descriptor(sender, path, descriptor), + ImportExportType::ExportLabels => export_labels(sender, daemon, path).await, ImportExportType::ImportPsbt => import_psbt(sender, path), ImportExportType::ImportDescriptor => import_descriptor(sender, path), }; @@ -538,6 +550,54 @@ pub fn import_descriptor(sender: Sender, path: PathBuf) { send_progress!(sender, Descriptor(descriptor)); } +pub async fn export_labels( + sender: Sender, + daemon: Option>, + path: PathBuf, +) { + let daemon = match daemon { + Some(d) => d, + None => { + send_error!(sender, Error::DaemonMissing); + return; + } + }; + let mut labels = Labels::new(Vec::new()); + let mut offset = 0u32; + loop { + let mut fetched = match daemon.get_labels_bip329(offset, DUMP_LABELS_LIMIT).await { + Ok(l) => l, + Err(e) => { + send_error!(sender, e.into()); + return; + } + } + .into_vec(); + let fetch_len = fetched.len() as u32; + labels.append(&mut fetched); + if fetch_len < DUMP_LABELS_LIMIT { + break; + } else { + offset += DUMP_LABELS_LIMIT; + } + } + let json = match labels.export() { + Ok(j) => j, + Err(e) => { + send_error!(sender, e.into()); + return; + } + }; + let mut file = open_file!(path, sender); + + if let Err(e) = file.write_all(json.as_bytes()) { + send_error!(sender, e.into()); + return; + } + send_progress!(sender, Progress(100.0)); + send_progress!(sender, Ended); +} + pub async fn get_path(filename: String) -> Option { rfd::AsyncFileDialog::new() .set_title("Choose a location to export...") diff --git a/liana-gui/src/lianalite/client/backend/api.rs b/liana-gui/src/lianalite/client/backend/api.rs index 2c45c2ba..606ba1d5 100644 --- a/liana-gui/src/lianalite/client/backend/api.rs +++ b/liana-gui/src/lianalite/client/backend/api.rs @@ -333,6 +333,11 @@ pub struct ListPsbts { pub psbts: Vec, } +#[derive(Deserialize)] +pub struct Labels { + pub labels: lianad::bip329::Labels, +} + #[derive(Deserialize)] pub struct Address { #[serde(deserialize_with = "deser_addr_assume_checked")] diff --git a/liana-gui/src/lianalite/client/backend/mod.rs b/liana-gui/src/lianalite/client/backend/mod.rs index 0e75184f..e141bd53 100644 --- a/liana-gui/src/lianalite/client/backend/mod.rs +++ b/liana-gui/src/lianalite/client/backend/mod.rs @@ -1117,9 +1117,22 @@ impl Daemon for BackendWalletClient { Ok(()) } - async fn get_labels_bip329(&self, _offset: u32, _limit: u32) -> Result { - // TODO: add an endpoint on backend - todo!() + async fn get_labels_bip329(&self, offset: u32, limit: u32) -> Result { + let response: Response = self + .inner + .request( + Method::GET, + &format!( + "{}/v1/wallets/{}/labels/bip329?offset={}&limit={}", + self.inner.url, self.wallet_uuid, offset, limit + ), + ) + .await + .send() + .await?; + + let res: api::Labels = response.json().await?; + Ok(res.labels) } } From f7ed341d284d0563a93314be439a8facff971767 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Tue, 4 Mar 2025 06:36:11 +0100 Subject: [PATCH 08/44] gui: Daemon trait => add receive and change indexes to GetInfoResult --- liana-gui/src/app/state/psbt.rs | 2 ++ liana-gui/src/lianalite/client/backend/api.rs | 2 ++ liana-gui/src/lianalite/client/backend/mod.rs | 2 ++ lianad/src/commands/mod.rs | 8 ++++++++ 4 files changed, 14 insertions(+) diff --git a/liana-gui/src/app/state/psbt.rs b/liana-gui/src/app/state/psbt.rs index df074374..cc6f2a7e 100644 --- a/liana-gui/src/app/state/psbt.rs +++ b/liana-gui/src/app/state/psbt.rs @@ -754,6 +754,8 @@ mod tests { "block_height": 1000, "sync": 1.0, "descriptors": { "main": LianaDescriptor::from_str(DESC).unwrap() }, + "receive_index": 4, + "change_index": 3, "timestamp": 1000, })), ), diff --git a/liana-gui/src/lianalite/client/backend/api.rs b/liana-gui/src/lianalite/client/backend/api.rs index 606ba1d5..131e04c9 100644 --- a/liana-gui/src/lianalite/client/backend/api.rs +++ b/liana-gui/src/lianalite/client/backend/api.rs @@ -105,6 +105,8 @@ pub struct Wallet { pub name: String, #[serde(deserialize_with = "deser_fromstr")] pub descriptor: LianaDescriptor, + pub deposit_derivation_index: u32, + pub change_derivation_index: u32, pub recovery_paths: Vec, pub biggest_remaining_sequence: Option, pub smallest_remaining_sequence: Option, diff --git a/liana-gui/src/lianalite/client/backend/mod.rs b/liana-gui/src/lianalite/client/backend/mod.rs index e141bd53..62d74019 100644 --- a/liana-gui/src/lianalite/client/backend/mod.rs +++ b/liana-gui/src/lianalite/client/backend/mod.rs @@ -594,6 +594,8 @@ impl Daemon for BackendWalletClient { timestamp: wallet.created_at as u32, // We can ignore this field for remote backend as the wallet should remain synced. last_poll_timestamp: None, + receive_index: wallet.deposit_derivation_index, + change_index: wallet.change_derivation_index, }) } diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index d197f437..ff0e223e 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -314,6 +314,8 @@ impl DaemonControl { let mut db_conn = self.db.connection(); let block_height = db_conn.chain_tip().map(|tip| tip.height).unwrap_or(0); let wallet = db_conn.wallet(); + let receive_index: u32 = db_conn.receive_index().into(); + let change_index: u32 = db_conn.change_index().into(); let rescan_progress = wallet .rescan_timestamp .map(|_| self.bitcoin.rescan_progress().unwrap_or(1.0)); @@ -328,6 +330,8 @@ impl DaemonControl { rescan_progress, timestamp: wallet.timestamp, last_poll_timestamp: wallet.last_poll_timestamp, + receive_index, + change_index, } } @@ -1168,6 +1172,10 @@ pub struct GetInfoResult { pub timestamp: u32, /// Timestamp of last poll, if any. pub last_poll_timestamp: Option, + /// Last index used to generate a receive address + pub receive_index: u32, + /// Last index used to generate a change address + pub change_index: u32, } #[derive(Debug, Clone, Serialize, Deserialize)] From f1b62074d309059c4a8b5cc1d954c25bb7cff0d7 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Tue, 25 Feb 2025 13:54:17 +0100 Subject: [PATCH 09/44] gui: implement backup --- liana-gui/src/app/settings.rs | 69 +++++- liana-gui/src/app/wallet.rs | 22 ++ liana-gui/src/backup.rs | 393 +++++++++++++++++++++++++++++++++ liana-gui/src/installer/mod.rs | 7 +- liana-gui/src/lib.rs | 1 + liana-gui/src/node/bitcoind.rs | 5 +- 6 files changed, 490 insertions(+), 7 deletions(-) create mode 100644 liana-gui/src/backup.rs diff --git a/liana-gui/src/app/settings.rs b/liana-gui/src/app/settings.rs index b4376154..27471b5b 100644 --- a/liana-gui/src/app/settings.rs +++ b/liana-gui/src/app/settings.rs @@ -6,9 +6,15 @@ use std::io::Write; use std::path::PathBuf; use liana::miniscript::bitcoin::{bip32::Fingerprint, Network}; +use liana_ui::component::form; use serde::{Deserialize, Serialize}; -use crate::{hw::HardwareWalletConfig, lianalite::client::backend, services}; +use crate::{ + backup::{Key, KeyRole, KeyType}, + hw::HardwareWalletConfig, + lianalite::client::backend, + services, +}; pub const DEFAULT_FILE_NAME: &str = "settings.json"; @@ -151,6 +157,67 @@ pub struct KeySetting { pub provider_key: Option, } +impl KeySetting { + pub fn to_backup(&self) -> Key { + if let Some(provider_key) = &self.provider_key { + if let Ok(metadata) = serde_json::to_value(provider_key) { + return Key { + key: self.master_fingerprint, + alias: Some(self.name.clone()), + role: None, + key_type: Some(KeyType::ThirdParty), + metadata, + }; + } + } + Key { + key: self.master_fingerprint, + alias: Some(self.name.clone()), + role: None, + key_type: None, + metadata: serde_json::Value::Null, + } + } + + pub fn from_backup( + name: String, + fg: Fingerprint, + _role: Option, + key_type: Option, + metadata: serde_json::Value, + ) -> Option { + if let Some(KeyType::ThirdParty) = key_type { + let provider_key = serde_json::from_value(metadata).ok(); + Some(Self { + name, + master_fingerprint: fg, + provider_key, + }) + } else { + Some(Self { + name, + master_fingerprint: fg, + provider_key: None, + }) + } + } + + pub fn to_form(&self) -> form::Value { + form::Value { + value: self.name.clone(), + valid: true, + } + } + + pub fn name(&self) -> String { + self.name.clone() + } + + pub fn has_name(&self) -> bool { + !self.name.is_empty() + } +} + #[derive(PartialEq, Eq, Debug, Clone)] pub enum SettingsError { NotFound, diff --git a/liana-gui/src/app/wallet.rs b/liana-gui/src/app/wallet.rs index 9313e271..52a095f1 100644 --- a/liana-gui/src/app/wallet.rs +++ b/liana-gui/src/app/wallet.rs @@ -179,6 +179,28 @@ impl Wallet { Ok(self) } } + + pub fn keys(&self) -> HashMap { + let mut map = HashMap::new(); + self.keys_aliases.iter().for_each(|(fg, alias)| { + map.insert( + *fg, + settings::KeySetting { + name: alias.clone(), + master_fingerprint: *fg, + provider_key: None, + }, + ); + }); + + self.provider_keys.iter().for_each(|(fg, key)| { + if let Some(entry) = map.get_mut(fg) { + entry.provider_key = Some(key.clone()) + } + }); + + map + } } #[allow(clippy::large_enum_variant)] diff --git a/liana-gui/src/backup.rs b/liana-gui/src/backup.rs new file mode 100644 index 00000000..d8577dac --- /dev/null +++ b/liana-gui/src/backup.rs @@ -0,0 +1,393 @@ +use chrono::{Duration, Utc}; +use liana::miniscript::{ + self, + bitcoin::{bip32::Fingerprint, Network, Txid}, +}; +use lianad::bip329; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::{ + collections::{BTreeMap, HashMap}, + fmt::{Debug, Display}, + path::PathBuf, + sync::Arc, + time::{SystemTime, UNIX_EPOCH}, +}; + +use crate::{ + app::{settings::Settings, wallet::Wallet, Config}, + daemon::{model::HistoryTransaction, Daemon, DaemonBackend, DaemonError}, + installer::{ + extract_daemon_config, extract_local_gui_settings, extract_remote_gui_settings, Context, + RemoteBackend, + }, + lianalite::client::backend::api::DEFAULT_LIMIT, +}; + +const CONFIG_KEY: &str = "config"; +const SETTINGS_KEY: &str = "settings"; + +fn now() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("cannot fail") + .as_secs() +} + +#[derive(Serialize, Deserialize)] +pub struct Backup { + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + pub accounts: Vec, + pub network: Network, + pub date: u64, + /// App proprietary metadata (settings, configuration, etc..) + #[serde(skip_serializing_if = "serde_json::Map::is_empty")] + pub proprietary: serde_json::Map, +} + +pub enum Error { + DescriptorMissing, + NotSingleWallet, + Json, + SettingsFromFile, + Daemon(DaemonError), + TxTimeMissing, +} + +impl From for Error { + fn from(value: DaemonError) -> Self { + Error::Daemon(value) + } +} + +impl Display for Backup { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = serde_json::to_string(self).map_err(|_| std::fmt::Error)?; + write!(f, "{str}") + } +} + +impl Debug for Backup { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = serde_json::to_string_pretty(self).map_err(|_| std::fmt::Error)?; + write!(f, "{str}") + } +} + +impl Backup { + /// Create a Backup from the Installer context + /// + /// # Arguments + /// * `ctx` - the installer context + /// * `timestamp` - whether to record the current timestamp as wallet creation time + /// (we should want to set timestamp = false for a wallet import for instance) + pub async fn from_installer(ctx: Context, timestamp: bool) -> Result { + let descriptor = ctx + .descriptor + .clone() + .ok_or(Error::DescriptorMissing)? + .to_string(); + + let now = now(); + + let mut account = Account::new(descriptor); + + let mut proprietary = serde_json::Map::new(); + + let config = extract_daemon_config(&ctx); + if let Ok(config) = serde_json::to_value(config) { + proprietary.insert(CONFIG_KEY.to_string(), config); + } + let settings = if ctx.bitcoin_backend.is_some() { + Some(extract_local_gui_settings(&ctx)) + } else { + match &ctx.remote_backend { + RemoteBackend::WithWallet(backend) => { + Some(extract_remote_gui_settings(&ctx, backend).await) + } + _ => None, + } + }; + + let name = if let Some(settings) = settings { + assert_eq!(settings.wallets.len(), 1); + if settings.wallets.len() != 1 { + return Err(Error::NotSingleWallet); + } + let settings = settings.wallets.first().expect("only one wallet"); + let name = settings.name.clone(); + if let Ok(settings) = serde_json::to_value(settings) { + proprietary.insert(SETTINGS_KEY.to_string(), settings); + } + Some(name) + } else { + None + }; + + ctx.keys.iter().for_each(|(k, s)| { + account.keys.insert(*k, s.to_backup()); + }); + + account.proprietary = proprietary; + account.name = name.clone(); + if timestamp { + account.timestamp = Some(now); + } + + Ok(Backup { + name, + accounts: vec![account], + network: ctx.network, + proprietary: serde_json::Map::new(), + date: now, + }) + } + + /// Create a Backup from the Liana App context + pub async fn from_app( + datadir: PathBuf, + network: Network, + config: Config, + wallet: Arc, + daemon: Arc, + ) -> Result { + let mut proprietary = serde_json::Map::new(); + let name = wallet.name.clone(); + let descriptor = wallet.main_descriptor.to_string(); + let keys = wallet.keys(); + + let settings = + Settings::from_file(datadir, network).map_err(|_| Error::SettingsFromFile)?; + if settings.wallets.len() == 1 { + if let Ok(settings) = serde_json::to_value(settings.wallets[0].clone()) { + proprietary.insert(SETTINGS_KEY.to_string(), settings); + } + } + + if let Ok(config) = serde_json::to_value(config) { + proprietary.insert(CONFIG_KEY.to_string(), config); + } + + let mut account = Account::new(descriptor); + account.proprietary = proprietary; + account.name = Some(name.clone()); + let info = daemon.get_info().await?; + account.timestamp = Some(info.timestamp as u64); + account.change_index = Some(info.change_index); + account.receive_index = Some(info.receive_index); + for (fg, setting) in keys { + account.keys.insert(fg, setting.to_backup()); + } + + const MAX_LABEL_BIP329: u32 = 100; + + let labels = { + let mut buff = Vec::new(); + let mut start = 0; + loop { + let mut fetched = daemon.get_labels_bip329(start, 100).await?.into_vec(); + + if fetched.len() < MAX_LABEL_BIP329 as usize { + buff.append(&mut fetched); + break; + } else { + buff.append(&mut fetched); + start += MAX_LABEL_BIP329; + } + } + bip329::Labels::new(buff) + }; + + account.labels = Some(labels); + account.transactions = get_transactions(&daemon) + .await? + .into_iter() + .map(|tx| miniscript::bitcoin::consensus::encode::serialize_hex(&tx.tx)) + .collect(); + account.psbts = daemon + .list_spend_transactions(None) + .await? + .into_iter() + .map(|tx| tx.psbt.serialize_hex()) + .collect(); + + Ok(Backup { + name: Some(name), + accounts: vec![account], + network, + proprietary: serde_json::Map::new(), + date: now(), + }) + } + + fn account(&self) -> Result<&Account, Error> { + if self.accounts.len() != 1 { + Err(Error::NotSingleWallet) + } else { + Ok(self.accounts.first().expect("single account")) + } + } + + pub fn config(&self) -> Result, Error> { + let account = self.account()?; + if let Some(config) = account.proprietary.get(CONFIG_KEY) { + let config: Config = serde_json::from_value(config.clone()).map_err(|_| Error::Json)?; + Ok(Some(config)) + } else { + Ok(None) + } + } + + pub fn settings(&self) -> Result, Error> { + let account = self.account()?; + if let Some(settings) = account.proprietary.get(SETTINGS_KEY) { + let settings: Settings = + serde_json::from_value(settings.clone()).map_err(|_| Error::Json)?; + Ok(Some(settings)) + } else { + Ok(None) + } + } +} + +async fn get_transactions( + daemon: &Arc, +) -> Result, Error> { + let max = match daemon.backend() { + DaemonBackend::RemoteBackend => DEFAULT_LIMIT as u64, + _ => u32::MAX as u64, + }; + + // look 2 hour forward + // https://github.com/bitcoin/bitcoin/blob/62bd61de110b057cbfd6e31e4d0b727d93119c72/src/chain.h#L29 + let mut end = ((Utc::now() + Duration::hours(2)).timestamp()) as u32; + + // store txs in a map to avoid duplicates + let mut map = HashMap::::new(); + let mut limit = max; + + loop { + let history_txs = daemon.list_history_txs(0, end, limit).await?; + // all txs have been fetched + if history_txs.is_empty() { + return Ok(Vec::new()); + } + + if history_txs.len() == limit as usize { + let first = if let Some(t) = history_txs.first().expect("checked").time { + t + } else { + return Err(Error::TxTimeMissing); + }; + + let last = if let Some(t) = history_txs.last().expect("checked").time { + t + } else { + return Err(Error::TxTimeMissing); + }; + + // limit too low, all tx are in the same timestamp + // we must increase limit and retry + if first == last { + limit += DEFAULT_LIMIT as u64; + continue; + } else { + // add txs to map + for tx in history_txs { + let txid = tx.txid; + map.insert(txid, tx); + } + limit = max; + end = first.min(last); + continue; + } + } else + /* history_txs.len() < limit */ + { + // add txs to map + for tx in history_txs { + let txid = tx.txid; + map.insert(txid, tx); + } + break; + } + } + let vec: Vec<_> = map.into_values().collect(); + Ok(vec) +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Account { + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + pub descriptor: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub receive_index: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub change_index: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub timestamp: Option, + #[serde(skip_serializing_if = "BTreeMap::is_empty")] + pub keys: BTreeMap, + #[serde(skip_serializing_if = "Option::is_none")] + pub labels: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub transactions: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub psbts: Vec, + #[serde(skip_serializing_if = "serde_json::Map::is_empty")] + pub proprietary: serde_json::Map, +} + +impl Account { + pub fn new(descriptor: String) -> Self { + Self { + name: None, + descriptor, + receive_index: None, + change_index: None, + timestamp: None, + keys: BTreeMap::new(), + labels: None, + transactions: Vec::new(), + psbts: Vec::new(), + proprietary: serde_json::Map::new(), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Key { + pub key: Fingerprint, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub alias: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub role: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub key_type: Option, + #[serde(default, skip_serializing_if = "Value::is_null")] + pub metadata: Value, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum KeyRole { + /// Key to be used in normal spending condition + Main, + /// Key that will be used for recover in case loss of main key(s) + Recovery, + /// Key that wil inherit coins if main user disapear + Inheritance, + /// Key that will cosign a spend in order to enforce some policy + Cosigning, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum KeyType { + /// Main user + Internal, + /// Heirs or friends + External, + /// Service the user pay for + ThirdParty, +} diff --git a/liana-gui/src/installer/mod.rs b/liana-gui/src/installer/mod.rs index 9c1fa3c5..4c946087 100644 --- a/liana-gui/src/installer/mod.rs +++ b/liana-gui/src/installer/mod.rs @@ -5,6 +5,7 @@ mod prompt; mod step; mod view; +pub use context::{Context, RemoteBackend}; use iced::{clipboard, Subscription, Task}; use liana::miniscript::bitcoin::{self, Network}; use liana_ui::{ @@ -14,8 +15,6 @@ use liana_ui::{ use lianad::config::Config; use tracing::{error, info, warn}; -use context::{Context, RemoteBackend}; - use std::io::Write; use std::path::PathBuf; use std::sync::{Arc, Mutex}; @@ -429,7 +428,7 @@ pub async fn install_local_wallet( info!("Gui configuration file created"); // create liana GUI settings file - let settings: gui_settings::Settings = extract_local_gui_settings(&ctx).await; + let settings: gui_settings::Settings = extract_local_gui_settings(&ctx); create_and_write_file( network_datadir_path, gui_settings::DEFAULT_FILE_NAME, @@ -669,7 +668,7 @@ pub async fn extract_remote_gui_settings(ctx: &Context, backend: &BackendWalletC } } -pub async fn extract_local_gui_settings(ctx: &Context) -> Settings { +pub fn extract_local_gui_settings(ctx: &Context) -> Settings { let descriptor = ctx .descriptor .as_ref() diff --git a/liana-gui/src/lib.rs b/liana-gui/src/lib.rs index f0160fa1..411db157 100644 --- a/liana-gui/src/lib.rs +++ b/liana-gui/src/lib.rs @@ -1,4 +1,5 @@ pub mod app; +pub mod backup; pub mod daemon; pub mod datadir; pub mod download; diff --git a/liana-gui/src/node/bitcoind.rs b/liana-gui/src/node/bitcoind.rs index 955c5898..59831507 100644 --- a/liana-gui/src/node/bitcoind.rs +++ b/liana-gui/src/node/bitcoind.rs @@ -6,6 +6,7 @@ use liana::{ }; use liana_ui::component::form; use lianad::config::BitcoindConfig; +use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::fmt; use std::path::{Path, PathBuf}; @@ -154,7 +155,7 @@ impl std::fmt::Display for RpcAuthParseError { } /// Represents RPC auth credentials as stored in bitcoin.conf. -#[derive(PartialEq, Eq, Debug, Clone)] +#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] pub struct RpcAuth { pub user: String, salt: String, @@ -209,7 +210,7 @@ impl std::str::FromStr for RpcAuth { } /// Represents section for a single network in `bitcoin.conf` file. -#[derive(PartialEq, Eq, Debug, Clone)] +#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] pub struct InternalBitcoindNetworkConfig { pub rpc_port: u16, pub p2p_port: u16, From f41de56e38164d9a9bad78a0abffaebdd92e6768 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Mon, 10 Mar 2025 14:35:55 +0100 Subject: [PATCH 10/44] gui: add import/export features in settings --- liana-gui/src/app/message.rs | 6 + liana-gui/src/app/mod.rs | 6 +- liana-gui/src/app/state/export.rs | 23 ++- liana-gui/src/app/state/mod.rs | 2 +- liana-gui/src/app/state/settings/mod.rs | 154 ++++++++++++++++++++- liana-gui/src/app/state/settings/wallet.rs | 149 +++++++++++++++++--- liana-gui/src/app/state/transactions.rs | 2 +- liana-gui/src/app/view/export.rs | 2 +- liana-gui/src/app/view/message.rs | 8 ++ liana-gui/src/app/view/settings.rs | 75 ++++++++++ liana-gui/src/backup.rs | 5 +- liana-gui/src/export.rs | 6 +- 12 files changed, 394 insertions(+), 44 deletions(-) diff --git a/liana-gui/src/app/message.rs b/liana-gui/src/app/message.rs index 96c68dc6..e7afe426 100644 --- a/liana-gui/src/app/message.rs +++ b/liana-gui/src/app/message.rs @@ -49,3 +49,9 @@ pub enum Message { RbfModal(Box, bool, Result, Error>), Export(ImportExportMessage), } + +impl From for Message { + fn from(value: ImportExportMessage) -> Self { + Message::View(view::Message::ImportExport(value)) + } +} diff --git a/liana-gui/src/app/mod.rs b/liana-gui/src/app/mod.rs index 80680ebd..004a5193 100644 --- a/liana-gui/src/app/mod.rs +++ b/liana-gui/src/app/mod.rs @@ -62,6 +62,7 @@ impl Panels { data_dir: PathBuf, daemon_backend: DaemonBackend, internal_bitcoind: Option<&Bitcoind>, + config: Arc, ) -> Panels { Self { current: Menu::Home, @@ -93,6 +94,7 @@ impl Panels { wallet.clone(), daemon_backend, internal_bitcoind.is_some(), + config.clone(), ), } } @@ -132,7 +134,7 @@ impl Panels { pub struct App { cache: Cache, - config: Config, + config: Arc, wallet: Arc, daemon: Arc, internal_bitcoind: Option, @@ -149,12 +151,14 @@ impl App { data_dir: PathBuf, internal_bitcoind: Option, ) -> (App, Task) { + let config = Arc::new(config); let mut panels = Panels::new( &cache, wallet.clone(), data_dir, daemon.backend(), internal_bitcoind.as_ref(), + config.clone(), ); let cmd = panels.home.reload(daemon.clone(), wallet.clone()); ( diff --git a/liana-gui/src/app/state/export.rs b/liana-gui/src/app/state/export.rs index 163c5eea..036b55dd 100644 --- a/liana-gui/src/app/state/export.rs +++ b/liana-gui/src/app/state/export.rs @@ -8,10 +8,7 @@ use liana_ui::{component::modal::Modal, widget::Element}; use tokio::task::JoinHandle; use crate::{ - app::{ - self, - view::{self, export::export_modal}, - }, + app::view::{self, export::export_modal}, daemon::Daemon, export::{self, get_path, ImportExportMessage, ImportExportState, ImportExportType, Progress}, }; @@ -58,16 +55,20 @@ impl ExportModal { ImportExportType::ImportPsbt => "psbt.psbt".into(), ImportExportType::ImportDescriptor => "descriptor.descriptor".into(), ImportExportType::ExportLabels => format!("liana-labels-{date}.csv"), + ImportExportType::ExportBackup(_) => format!("liana-backup-{date}.json"), } } - pub fn launch(&self) -> Task { - Task::perform(get_path(self.default_filename()), |m| { - app::message::Message::View(view::Message::ImportExport(ImportExportMessage::Path(m))) + pub fn launch + Send + 'static>(&self) -> Task { + Task::perform(get_path(self.default_filename()), move |m| { + ImportExportMessage::Path(m).into() }) } - pub fn update(&mut self, message: ImportExportMessage) -> Task { + pub fn update + Send + 'static>( + &mut self, + message: ImportExportMessage, + ) -> Task { match message { ImportExportMessage::Progress(m) => match m { Progress::Started(handle) => { @@ -106,11 +107,7 @@ impl ExportModal { self.path = Some(path); self.start(); } else { - return Task::perform(async {}, |_| { - app::message::Message::View(view::Message::ImportExport( - ImportExportMessage::Close, - )) - }); + return Task::perform(async {}, |_| ImportExportMessage::Close.into()); } } ImportExportMessage::Close | ImportExportMessage::Open => { /* unreachable */ } diff --git a/liana-gui/src/app/state/mod.rs b/liana-gui/src/app/state/mod.rs index e76c274b..a54f0397 100644 --- a/liana-gui/src/app/state/mod.rs +++ b/liana-gui/src/app/state/mod.rs @@ -1,5 +1,5 @@ mod coins; -mod export; +pub mod export; mod label; mod psbt; mod psbts; diff --git a/liana-gui/src/app/state/settings/mod.rs b/liana-gui/src/app/state/settings/mod.rs index 64ec0b27..9a22cd9d 100644 --- a/liana-gui/src/app/state/settings/mod.rs +++ b/liana-gui/src/app/state/settings/mod.rs @@ -20,16 +20,22 @@ use crate::{ state::State, view::{self}, wallet::Wallet, + Config, }, + backup::Backup, daemon::{Daemon, DaemonBackend}, + export::{ImportExportMessage, ImportExportType}, }; +use super::export::ExportModal; + pub struct SettingsState { data_dir: PathBuf, wallet: Arc, setting: Option>, daemon_backend: DaemonBackend, internal_bitcoind: bool, + config: Arc, } impl SettingsState { @@ -38,6 +44,7 @@ impl SettingsState { wallet: Arc, daemon_backend: DaemonBackend, internal_bitcoind: bool, + config: Arc, ) -> Self { Self { data_dir, @@ -45,6 +52,7 @@ impl SettingsState { setting: None, daemon_backend, internal_bitcoind, + config, } } } @@ -79,6 +87,12 @@ impl State for SettingsState { self.setting = Some(BackendSettingsState::new().into()); Task::none() } + Message::View(view::Message::Settings(view::SettingsMessage::ImportExportSection)) => { + self.setting = Some( + ImportExportSettingsState::new(self.wallet.clone(), self.config.clone()).into(), + ); + Task::none() + } Message::View(view::Message::Settings(view::SettingsMessage::AboutSection)) => { self.setting = Some(AboutSettingsState::default().into()); let wallet = self.wallet.clone(); @@ -89,7 +103,12 @@ impl State for SettingsState { } Message::View(view::Message::Settings(view::SettingsMessage::EditWalletSettings)) => { self.setting = Some( - WalletSettingsState::new(self.data_dir.clone(), self.wallet.clone()).into(), + WalletSettingsState::new( + self.data_dir.clone(), + self.wallet.clone(), + self.config.clone(), + ) + .into(), ); let wallet = self.wallet.clone(); self.setting @@ -145,6 +164,139 @@ impl From for Box { } } +pub struct ImportExportSettingsState { + warning: Option, + modal: Option, + wallet: Arc, + config: Arc, +} + +impl ImportExportSettingsState { + pub fn new(wallet: Arc, config: Arc) -> Self { + Self { + warning: None, + modal: None, + wallet, + config, + } + } +} + +macro_rules! launch { + ($s:ident, $m: ident) => { + let launch = $m.launch(); + $s.modal = Some($m); + return launch + }; +} + +impl State for ImportExportSettingsState { + fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> { + let content = view::settings::import_export(cache, self.warning.as_ref()); + if let Some(modal) = &self.modal { + modal.view(content) + } else { + content + } + } + + fn subscription(&self) -> iced::Subscription { + if let Some(modal) = &self.modal { + if let Some(sub) = modal.subscription() { + return sub.map(|m| { + Message::View(view::Message::Settings( + view::SettingsMessage::ImportExport(ImportExportMessage::Progress(m)), + )) + }); + } + } + iced::Subscription::none() + } + + fn update( + &mut self, + daemon: Arc, + cache: &Cache, + message: Message, + ) -> Task { + match message { + Message::View(view::Message::ImportExport(ImportExportMessage::Close)) => { + self.modal = None; + } + Message::View(view::Message::ImportExport(m)) => { + if let Some(modal) = self.modal.as_mut() { + return modal.update(m); + }; + } + Message::View(view::Message::Settings(view::SettingsMessage::ImportExport(m))) => { + if let Some(modal) = self.modal.as_mut() { + return modal.update(m); + }; + } + Message::View(view::Message::Settings(view::SettingsMessage::ExportDescriptor)) => { + if self.modal.is_none() { + let modal = ExportModal::new( + daemon, + ImportExportType::Descriptor(self.wallet.main_descriptor.clone()), + ); + launch!(self, modal); + } + } + Message::View(view::Message::Settings(view::SettingsMessage::ExportTransactions)) => { + if self.modal.is_none() { + let modal = ExportModal::new(daemon, ImportExportType::Transactions); + launch!(self, modal); + } + } + Message::View(view::Message::Settings(view::SettingsMessage::ExportLabels)) => { + if self.modal.is_none() { + let modal = ExportModal::new(daemon, ImportExportType::ExportLabels); + launch!(self, modal); + } + } + Message::View(view::Message::Settings(view::SettingsMessage::ExportWallet)) => { + if self.modal.is_none() { + let datadir = cache.datadir_path.clone(); + let network = cache.network; + let config = self.config.clone(); + let wallet = self.wallet.clone(); + let daemon = daemon.clone(); + return Task::perform( + async move { + let backup = + Backup::from_app(datadir, network, config, wallet, daemon).await; + let backup = backup.unwrap(); + serde_json::to_string_pretty(&backup).unwrap() + // TODO: do not unwrap, return an error message instead + }, + |s| { + Message::View(view::Message::Settings( + view::SettingsMessage::ExportBackup(s), + )) + }, + ); + } + } + Message::View(view::Message::Settings(view::SettingsMessage::ExportBackup(backup))) => { + let modal = ExportModal::new(daemon, ImportExportType::ExportBackup(backup)); + launch!(self, modal); + } + Message::View(view::Message::Settings(view::SettingsMessage::ImportWallet)) => { + // TODO: + } + _ => {} + } + + Task::none() + } +} + +impl From for Box { + fn from(s: ImportExportSettingsState) -> Box { + Box::new(s) + } +} + #[derive(Default)] pub struct AboutSettingsState { daemon_version: Option, diff --git a/liana-gui/src/app/state/settings/wallet.rs b/liana-gui/src/app/state/settings/wallet.rs index f5281fae..21e546d6 100644 --- a/liana-gui/src/app/state/settings/wallet.rs +++ b/liana-gui/src/app/state/settings/wallet.rs @@ -17,34 +17,57 @@ use liana_ui::{ use crate::{ app::{ - cache::Cache, error::Error, message::Message, settings, state::State, view, wallet::Wallet, + cache::Cache, + error::Error, + message::Message, + settings, + state::{export::ExportModal, State}, + view, + wallet::Wallet, + Config, }, + backup::Backup, daemon::{Daemon, DaemonBackend}, + export::{ImportExportMessage, ImportExportType}, hw::{HardwareWallet, HardwareWalletConfig, HardwareWallets}, }; +enum Modal { + None, + RegisterWallet(RegisterWalletModal), + ImportExport(ExportModal), +} + +impl Modal { + fn is_none(&self) -> bool { + matches!(self, Modal::None) + } +} + pub struct WalletSettingsState { data_dir: PathBuf, warning: Option, descriptor: LianaDescriptor, keys_aliases: Vec<(Fingerprint, form::Value)>, wallet: Arc, - modal: Option, + modal: Modal, processing: bool, updated: bool, + config: Arc, } impl WalletSettingsState { - pub fn new(data_dir: PathBuf, wallet: Arc) -> Self { + pub fn new(data_dir: PathBuf, wallet: Arc, config: Arc) -> Self { WalletSettingsState { data_dir, descriptor: wallet.main_descriptor.clone(), keys_aliases: Self::keys_aliases(&wallet), wallet, warning: None, - modal: None, + modal: Modal::None, processing: false, updated: false, + config, } } @@ -86,20 +109,31 @@ impl State for WalletSettingsState { self.processing, self.updated, ); - if let Some(m) = &self.modal { - modal::Modal::new(content, m.view()) + + match &self.modal { + Modal::None => content, + Modal::RegisterWallet(m) => modal::Modal::new(content, m.view()) .on_blur(Some(view::Message::Close)) - .into() - } else { - content + .into(), + Modal::ImportExport(m) => m.view(content), } } fn subscription(&self) -> Subscription { - if let Some(modal) = &self.modal { - modal.subscription() - } else { - Subscription::none() + match &self.modal { + Modal::None => Subscription::none(), + Modal::RegisterWallet(modal) => modal.subscription(), + Modal::ImportExport(modal) => { + if let Some(sub) = modal.subscription() { + sub.map(|m| { + Message::View(view::Message::Settings( + view::SettingsMessage::ImportExport(ImportExportMessage::Progress(m)), + )) + }) + } else { + Subscription::none() + } + } } } @@ -112,7 +146,7 @@ impl State for WalletSettingsState { match message { Message::WalletUpdated(res) => { self.processing = false; - if let Some(modal) = &mut self.modal { + if let Modal::RegisterWallet(modal) = &mut self.modal { modal.update(daemon, cache, Message::WalletUpdated(res)) } else { match res { @@ -139,7 +173,7 @@ impl State for WalletSettingsState { Task::none() } Message::View(view::Message::Settings(view::SettingsMessage::Save)) => { - self.modal = None; + self.modal = Modal::None; self.processing = true; self.updated = false; Task::perform( @@ -157,22 +191,93 @@ impl State for WalletSettingsState { ) } Message::View(view::Message::Close) => { - self.modal = None; + self.modal = Modal::None; Task::none() } Message::View(view::Message::Settings(view::SettingsMessage::RegisterWallet)) => { - self.modal = Some(RegisterWalletModal::new( + self.modal = Modal::RegisterWallet(RegisterWalletModal::new( self.data_dir.clone(), self.wallet.clone(), cache.network, )); Task::none() } - _ => self - .modal - .as_mut() - .map(|m| m.update(daemon, cache, message)) - .unwrap_or_else(Task::none), + + Message::View(view::Message::ImportExport(ImportExportMessage::Close)) => { + if let Modal::ImportExport(_) = &self.modal { + self.modal = Modal::None; + } + Task::none() + } + Message::View(view::Message::ImportExport(m)) => { + if let Modal::ImportExport(modal) = &mut self.modal { + modal.update(m) + } else { + Task::none() + } + } + Message::View(view::Message::Settings(view::SettingsMessage::ImportExport(m))) => { + if let Modal::ImportExport(modal) = &mut self.modal { + modal.update(m) + } else { + Task::none() + } + } + Message::View(view::Message::Settings(view::SettingsMessage::ExportWallet)) => { + if self.modal.is_none() { + let datadir = cache.datadir_path.clone(); + let network = cache.network; + let config = self.config.clone(); + let wallet = self.wallet.clone(); + let daemon = daemon.clone(); + Task::perform( + async move { + let backup = + Backup::from_app(datadir, network, config, wallet, daemon).await; + let backup = backup.unwrap(); + serde_json::to_string_pretty(&backup).unwrap() + // TODO: do not unwrap, return an error message instead + }, + |s| { + Message::View(view::Message::Settings( + view::SettingsMessage::ExportBackup(s), + )) + }, + ) + } else { + Task::none() + } + } + Message::View(view::Message::Settings(view::SettingsMessage::ExportBackup(backup))) => { + if self.modal.is_none() { + let modal = + // ExportModal::new(Some(daemon), ImportExportType::ExportBackup(backup)); + ExportModal::new(daemon, ImportExportType::ExportBackup(backup)); + let launch = modal.launch(); + self.modal = Modal::ImportExport(modal); + launch + } else { + Task::none() + } + } + Message::View(view::Message::Settings(view::SettingsMessage::ImportWallet)) => { + if self.modal.is_none() { + let modal = ExportModal::new( + // Some(daemon), + daemon, + ImportExportType::Descriptor(self.wallet.main_descriptor.clone()), + ); + let launch = modal.launch(); + self.modal = Modal::ImportExport(modal); + launch + } else { + Task::none() + } + } + _ => match &mut self.modal { + Modal::RegisterWallet(m) => m.update(daemon, cache, message), + _ => Task::none(), + }, } } diff --git a/liana-gui/src/app/state/transactions.rs b/liana-gui/src/app/state/transactions.rs index dfbf4353..a871cc8f 100644 --- a/liana-gui/src/app/state/transactions.rs +++ b/liana-gui/src/app/state/transactions.rs @@ -287,7 +287,7 @@ impl State for TransactionsPanel { TransactionsModal::CreateRbf(modal) => modal.update(daemon, _cache, message), TransactionsModal::Export(modal) => { if let Message::View(view::Message::ImportExport(m)) = msg { - modal.update(m.clone()) + modal.update::(m.clone()) } else { Task::none() } diff --git a/liana-gui/src/app/view/export.rs b/liana-gui/src/app/view/export.rs index 900cee8b..5a5673fe 100644 --- a/liana-gui/src/app/view/export.rs +++ b/liana-gui/src/app/view/export.rs @@ -64,7 +64,7 @@ pub fn export_modal<'a>( card::simple( Column::new() .spacing(10) - .push(Container::new(h4_bold(format!("Export {export_type}"))).width(Length::Fill)) + .push(Container::new(h4_bold(export_type)).width(Length::Fill)) .push(Space::with_height(Length::Fill)) .push(progress_bar_row) .push(Space::with_height(Length::Fill)) diff --git a/liana-gui/src/app/view/message.rs b/liana-gui/src/app/view/message.rs index a1e796c8..d7a46010 100644 --- a/liana-gui/src/app/view/message.rs +++ b/liana-gui/src/app/view/message.rs @@ -70,9 +70,17 @@ pub enum SettingsMessage { BitcoindSettings(SettingsEditMessage), ElectrumSettings(SettingsEditMessage), RescanSettings(SettingsEditMessage), + ImportExport(ImportExportMessage), EditRemoteBackendSettings, RemoteBackendSettings(RemoteBackendSettingsMessage), EditWalletSettings, + ImportExportSection, + ExportDescriptor, + ExportTransactions, + ExportLabels, + ExportWallet, + ExportBackup(String), + ImportWallet, AboutSection, RegisterWallet, FingerprintAliasEdited(Fingerprint, String), diff --git a/liana-gui/src/app/view/settings.rs b/liana-gui/src/app/view/settings.rs index ec0d2627..6835ea7c 100644 --- a/liana-gui/src/app/view/settings.rs +++ b/liana-gui/src/app/view/settings.rs @@ -106,6 +106,13 @@ pub fn list(cache: &Cache, is_remote_backend: bool) -> Element { Message::Settings(SettingsMessage::EditWalletSettings), ); + let import_export = settings_section( + "Import/export", + None, + icon::wallet_icon(), + Message::Settings(SettingsMessage::ImportExportSection), + ); + let recovery = settings_section( "Recovery", Some("In case of loss of the main key, the recovery key can move the funds after a certain time."), @@ -130,6 +137,7 @@ pub fn list(cache: &Cache, is_remote_backend: bool) -> Element { .push(header) .push(if !is_remote_backend { node } else { backend }) .push(wallet) + .push(import_export) .push(recovery) .push(about), ) @@ -153,6 +161,60 @@ pub fn bitcoind_settings<'a>( ) } +pub fn import_export<'a>(cache: &'a Cache, warning: Option<&Error>) -> Element<'a, Message> { + let header = header("Import/Export", SettingsMessage::ImportExportSection); + + let export_descriptor = settings_section( + "Export descriptor", + None, + icon::wallet_icon(), + Message::Settings(SettingsMessage::ExportDescriptor), + ); + + let export_transactions = settings_section( + "Export transactions", + None, + icon::wallet_icon(), + Message::Settings(SettingsMessage::ExportTransactions), + ); + + let export_labels = settings_section( + "Export labels", + None, + icon::wallet_icon(), + Message::Settings(SettingsMessage::ExportLabels), + ); + + let export_wallet = settings_section( + "Back Up Wallet", + None, + icon::wallet_icon(), + Message::Settings(SettingsMessage::ExportWallet), + ); + + let import_wallet = settings_section( + "Restore wallet", + None, + icon::wallet_icon(), + Message::Settings(SettingsMessage::ImportWallet), + ); + + dashboard( + &Menu::Settings, + cache, + warning, + Column::new() + .spacing(20) + .push(header) + .push(export_descriptor) + .push(export_transactions) + .push(export_labels) + .push(export_wallet) + .push(import_wallet) + .width(Length::Fill), + ) +} + pub fn about_section<'a>( cache: &'a Cache, warning: Option<&Error>, @@ -856,6 +918,18 @@ pub fn wallet_settings<'a>( ) -> Element<'a, Message> { let header = header("Wallet", SettingsMessage::EditWalletSettings); + let import_export = Row::new() + .push( + button::secondary(Some(icon::wallet_icon()), "Backup") + .on_press(Message::Settings(SettingsMessage::ExportWallet)), + ) + .push(Space::with_width(10)) + .push( + button::secondary(Some(icon::wallet_icon()), "Restore") + .on_press(Message::Settings(SettingsMessage::ImportWallet)), + ) + .push(Space::with_width(Length::Fill)); + let descr = card::simple( Column::new() .push(text("Wallet descriptor:").bold()) @@ -943,6 +1017,7 @@ pub fn wallet_settings<'a>( Column::new() .spacing(20) .push(header) + .push(import_export) .push(descr) .push( card::simple(display_policy( diff --git a/liana-gui/src/backup.rs b/liana-gui/src/backup.rs index d8577dac..86c4398d 100644 --- a/liana-gui/src/backup.rs +++ b/liana-gui/src/backup.rs @@ -46,6 +46,7 @@ pub struct Backup { pub proprietary: serde_json::Map, } +#[derive(Debug)] pub enum Error { DescriptorMissing, NotSingleWallet, @@ -148,7 +149,7 @@ impl Backup { pub async fn from_app( datadir: PathBuf, network: Network, - config: Config, + config: Arc, wallet: Arc, daemon: Arc, ) -> Result { @@ -165,7 +166,7 @@ impl Backup { } } - if let Ok(config) = serde_json::to_value(config) { + if let Ok(config) = serde_json::to_value((*config).clone()) { proprietary.insert(CONFIG_KEY.to_string(), config); } diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index 57baf7cc..7b114663 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -135,6 +135,7 @@ pub enum Error { pub enum ImportExportType { Transactions, ExportPsbt(String), + ExportBackup(String), Descriptor(LianaDescriptor), ExportLabels, ImportPsbt, @@ -218,11 +219,12 @@ impl Export { ) { match export_type { ImportExportType::Transactions => export_transactions(sender, daemon, path).await, - ImportExportType::ExportPsbt(psbt) => export_psbt(sender, path, psbt), + ImportExportType::ExportPsbt(str) => export_string(sender, path, str), ImportExportType::Descriptor(descriptor) => export_descriptor(sender, path, descriptor), ImportExportType::ExportLabels => export_labels(sender, daemon, path).await, ImportExportType::ImportPsbt => import_psbt(sender, path), ImportExportType::ImportDescriptor => import_descriptor(sender, path), + ImportExportType::ExportBackup(str) => export_string(sender, path, str), }; } @@ -497,7 +499,7 @@ pub fn export_descriptor(sender: Sender, path: PathBuf, descriptor: Li send_progress!(sender, Ended); } -pub fn export_psbt(sender: Sender, path: PathBuf, psbt: String) { +pub fn export_string(sender: Sender, path: PathBuf, psbt: String) { let mut file = open_file!(path, sender); if let Err(e) = file.write_all(psbt.as_bytes()) { From ab3c8007bac711c3e964fb21f192c03c5753b276 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Wed, 5 Mar 2025 05:12:16 +0100 Subject: [PATCH 11/44] gui: implement ExportModal.modal_title() --- liana-gui/src/app/state/export.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/liana-gui/src/app/state/export.rs b/liana-gui/src/app/state/export.rs index 036b55dd..89297779 100644 --- a/liana-gui/src/app/state/export.rs +++ b/liana-gui/src/app/state/export.rs @@ -36,6 +36,18 @@ impl ExportModal { } } + pub fn modal_title(&self) -> &'static str { + match self.import_export_type { + ImportExportType::Transactions => "Export Transactions", + ImportExportType::ExportPsbt(_) => "Export PSBT", + ImportExportType::ExportBackup(_) => "Export Backup", + ImportExportType::Descriptor(_) => "Export Descriptor", + ImportExportType::ExportLabels => "Export Labels", + ImportExportType::ImportPsbt => "Import PSBT", + ImportExportType::ImportDescriptor => "Import Descriptor", + } + } + pub fn default_filename(&self) -> String { let date = chrono::Local::now().format("%Y-%m-%dT%H-%M-%S"); match &self.import_export_type { @@ -117,7 +129,7 @@ impl ExportModal { pub fn view<'a>(&'a self, content: Element<'a, view::Message>) -> Element { let modal = Modal::new( content, - export_modal(&self.state, self.error.as_ref(), "Transactions"), + export_modal(&self.state, self.error.as_ref(), self.modal_title()), ); match self.state { ImportExportState::TimedOut @@ -145,7 +157,7 @@ impl ExportModal { match &self.state { ImportExportState::Started | ImportExportState::Progress(_) => { Some(iced::Subscription::run_with_id( - "transactions", + self.modal_title(), export::export_subscription( self.daemon.clone(), path.to_path_buf(), From 530b8c12bcd79075e39c7ff7fa289c4234d9fa15 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Tue, 4 Mar 2025 06:57:07 +0100 Subject: [PATCH 12/44] gui: clippyfy --- liana-gui/src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/liana-gui/src/main.rs b/liana-gui/src/main.rs index 72ef2814..e256be2c 100644 --- a/liana-gui/src/main.rs +++ b/liana-gui/src/main.rs @@ -574,6 +574,7 @@ fn main() -> Result<(), Box> { fonts: font::load(), }; + #[allow(unused_mut)] let mut window_settings = iced::window::Settings { icon: Some(image::liana_app_icon()), position: iced::window::Position::Default, From 5dcecc5542f3feeb989d40de2c8cc3063f1409d5 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Wed, 5 Mar 2025 04:55:27 +0100 Subject: [PATCH 13/44] gui: make ExportModal.daemon optionnal --- liana-gui/src/app/state/export.rs | 7 +++++-- liana-gui/src/app/state/settings/mod.rs | 8 ++++---- liana-gui/src/app/state/settings/wallet.rs | 4 ++-- liana-gui/src/app/state/transactions.rs | 2 +- liana-gui/src/export.rs | 4 ++-- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/liana-gui/src/app/state/export.rs b/liana-gui/src/app/state/export.rs index 89297779..4dc54fec 100644 --- a/liana-gui/src/app/state/export.rs +++ b/liana-gui/src/app/state/export.rs @@ -19,13 +19,16 @@ pub struct ExportModal { handle: Option>>>, state: ImportExportState, error: Option, - daemon: Arc, + daemon: Option>, import_export_type: ImportExportType, } impl ExportModal { #[allow(clippy::new_without_default)] - pub fn new(daemon: Arc, export_type: ImportExportType) -> Self { + pub fn new( + daemon: Option>, + export_type: ImportExportType, + ) -> Self { Self { path: None, handle: None, diff --git a/liana-gui/src/app/state/settings/mod.rs b/liana-gui/src/app/state/settings/mod.rs index 9a22cd9d..69b3117d 100644 --- a/liana-gui/src/app/state/settings/mod.rs +++ b/liana-gui/src/app/state/settings/mod.rs @@ -236,7 +236,7 @@ impl State for ImportExportSettingsState { Message::View(view::Message::Settings(view::SettingsMessage::ExportDescriptor)) => { if self.modal.is_none() { let modal = ExportModal::new( - daemon, + Some(daemon), ImportExportType::Descriptor(self.wallet.main_descriptor.clone()), ); launch!(self, modal); @@ -244,13 +244,13 @@ impl State for ImportExportSettingsState { } Message::View(view::Message::Settings(view::SettingsMessage::ExportTransactions)) => { if self.modal.is_none() { - let modal = ExportModal::new(daemon, ImportExportType::Transactions); + let modal = ExportModal::new(Some(daemon), ImportExportType::Transactions); launch!(self, modal); } } Message::View(view::Message::Settings(view::SettingsMessage::ExportLabels)) => { if self.modal.is_none() { - let modal = ExportModal::new(daemon, ImportExportType::ExportLabels); + let modal = ExportModal::new(Some(daemon), ImportExportType::ExportLabels); launch!(self, modal); } } @@ -278,7 +278,7 @@ impl State for ImportExportSettingsState { } } Message::View(view::Message::Settings(view::SettingsMessage::ExportBackup(backup))) => { - let modal = ExportModal::new(daemon, ImportExportType::ExportBackup(backup)); + let modal = ExportModal::new(Some(daemon), ImportExportType::ExportBackup(backup)); launch!(self, modal); } Message::View(view::Message::Settings(view::SettingsMessage::ImportWallet)) => { diff --git a/liana-gui/src/app/state/settings/wallet.rs b/liana-gui/src/app/state/settings/wallet.rs index 21e546d6..1315f373 100644 --- a/liana-gui/src/app/state/settings/wallet.rs +++ b/liana-gui/src/app/state/settings/wallet.rs @@ -252,7 +252,7 @@ impl State for WalletSettingsState { if self.modal.is_none() { let modal = // ExportModal::new(Some(daemon), ImportExportType::ExportBackup(backup)); - ExportModal::new(daemon, ImportExportType::ExportBackup(backup)); + ExportModal::new(Some(daemon), ImportExportType::ExportBackup(backup)); let launch = modal.launch(); self.modal = Modal::ImportExport(modal); launch @@ -264,7 +264,7 @@ impl State for WalletSettingsState { if self.modal.is_none() { let modal = ExportModal::new( // Some(daemon), - daemon, + Some(daemon), ImportExportType::Descriptor(self.wallet.main_descriptor.clone()), ); let launch = modal.launch(); diff --git a/liana-gui/src/app/state/transactions.rs b/liana-gui/src/app/state/transactions.rs index a871cc8f..03970daf 100644 --- a/liana-gui/src/app/state/transactions.rs +++ b/liana-gui/src/app/state/transactions.rs @@ -269,7 +269,7 @@ impl State for TransactionsPanel { Message::View(view::Message::ImportExport(ImportExportMessage::Open)) => { if let TransactionsModal::None = &self.modal { self.modal = TransactionsModal::Export(ExportModal::new( - daemon, + Some(daemon), ImportExportType::Transactions, )); if let TransactionsModal::Export(m) = &self.modal { diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index 7b114663..6e546b90 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -260,12 +260,12 @@ impl Export { } pub fn export_subscription( - daemon: Arc, + daemon: Option>, path: PathBuf, export_type: ImportExportType, ) -> impl Stream { iced::stream::channel(100, move |mut output| async move { - let mut state = Export::new(Some(daemon), Box::new(path), export_type); + let mut state = Export::new(daemon, Box::new(path), export_type); loop { match state.state() { Status::Init => { From b0e560c0b523fba9994697c1f410ce0245c8ce09 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Tue, 11 Mar 2025 20:59:27 +0100 Subject: [PATCH 14/44] export: make ExportModal::view() generic --- liana-gui/src/app/state/export.rs | 10 +++++++--- liana-gui/src/app/view/export.rs | 5 ++--- liana-gui/src/app/view/message.rs | 10 ++++++++++ liana-gui/src/installer/message.rs | 15 +++++++++++++++ 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/liana-gui/src/app/state/export.rs b/liana-gui/src/app/state/export.rs index 4dc54fec..5e8a01c3 100644 --- a/liana-gui/src/app/state/export.rs +++ b/liana-gui/src/app/state/export.rs @@ -8,7 +8,7 @@ use liana_ui::{component::modal::Modal, widget::Element}; use tokio::task::JoinHandle; use crate::{ - app::view::{self, export::export_modal}, + app::view::{export::export_modal, Close}, daemon::Daemon, export::{self, get_path, ImportExportMessage, ImportExportState, ImportExportType, Progress}, }; @@ -129,7 +129,11 @@ impl ExportModal { } Task::none() } - pub fn view<'a>(&'a self, content: Element<'a, view::Message>) -> Element { + + pub fn view<'a, M>(&'a self, content: Element<'a, M>) -> Element + where + M: 'a + Close + Clone + From, + { let modal = Modal::new( content, export_modal(&self.state, self.error.as_ref(), self.modal_title()), @@ -138,7 +142,7 @@ impl ExportModal { ImportExportState::TimedOut | ImportExportState::Aborted | ImportExportState::Ended - | ImportExportState::Closed => modal.on_blur(Some(view::Message::Close)), + | ImportExportState::Closed => modal.on_blur(Some(M::close())), _ => modal, } .into() diff --git a/liana-gui/src/app/view/export.rs b/liana-gui/src/app/view/export.rs index 5a5673fe..62c63d6f 100644 --- a/liana-gui/src/app/view/export.rs +++ b/liana-gui/src/app/view/export.rs @@ -11,11 +11,10 @@ use liana_ui::{ widget::Element, }; -use crate::export::{Error, ImportExportMessage}; -use crate::{app::view::message::Message, export::ImportExportState}; +use crate::export::{Error, ImportExportMessage, ImportExportState}; /// Return the modal view for an export task -pub fn export_modal<'a>( +pub fn export_modal<'a, Message: From + Clone + 'a>( state: &ImportExportState, error: Option<&'a Error>, export_type: &str, diff --git a/liana-gui/src/app/view/message.rs b/liana-gui/src/app/view/message.rs index d7a46010..6ff27a6c 100644 --- a/liana-gui/src/app/view/message.rs +++ b/liana-gui/src/app/view/message.rs @@ -1,6 +1,10 @@ use crate::{app::menu::Menu, export::ImportExportMessage, node::bitcoind::RpcAuthType}; use liana::miniscript::bitcoin::{bip32::Fingerprint, OutPoint}; +pub trait Close { + fn close() -> Self; +} + #[derive(Debug, Clone)] pub enum Message { Reload, @@ -22,6 +26,12 @@ pub enum Message { ImportExport(ImportExportMessage), } +impl Close for Message { + fn close() -> Self { + Self::Close + } +} + #[derive(Debug, Clone)] pub enum LabelMessage { Edited(String), diff --git a/liana-gui/src/installer/message.rs b/liana-gui/src/installer/message.rs index 56828ee6..414532ac 100644 --- a/liana-gui/src/installer/message.rs +++ b/liana-gui/src/installer/message.rs @@ -7,7 +7,9 @@ use std::path::PathBuf; use super::{context, Error}; use crate::{ app::settings::ProviderKey, + app::view::Close, download::{DownloadError, Progress}, + export::ImportExportMessage, hw::HardwareWalletMessage, installer::descriptor::{Key, PathKind}, lianalite::client::{auth::AuthClient, backend::api}, @@ -49,6 +51,19 @@ pub enum Message { RedeemNextKey, KeyRedeemed(ProviderKey, Result<(), services::Error>), AllKeysRedeemed, + ImportExport(ImportExportMessage), +} + +impl Close for Message { + fn close() -> Self { + Self::Close + } +} + +impl From for Message { + fn from(value: ImportExportMessage) -> Self { + Self::ImportExport(value) + } } #[derive(Debug, Clone)] From 68bb742755b3a277aa91b15f540ab6d8012e1384 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Wed, 5 Mar 2025 06:07:51 +0100 Subject: [PATCH 15/44] installer: add wallet export feature at descriptor backup step --- liana-gui/src/app/state/export.rs | 10 ++- liana-gui/src/app/state/settings/mod.rs | 6 +- liana-gui/src/app/state/settings/wallet.rs | 6 +- liana-gui/src/app/view/message.rs | 4 +- liana-gui/src/backup.rs | 6 +- liana-gui/src/installer/message.rs | 5 +- liana-gui/src/installer/mod.rs | 3 + .../src/installer/step/descriptor/mod.rs | 73 +++++++++++++++++-- liana-gui/src/installer/view/mod.rs | 17 ++++- 9 files changed, 108 insertions(+), 22 deletions(-) diff --git a/liana-gui/src/app/state/export.rs b/liana-gui/src/app/state/export.rs index 5e8a01c3..e6f3ac0c 100644 --- a/liana-gui/src/app/state/export.rs +++ b/liana-gui/src/app/state/export.rs @@ -69,7 +69,7 @@ impl ExportModal { } ImportExportType::ImportPsbt => "psbt.psbt".into(), ImportExportType::ImportDescriptor => "descriptor.descriptor".into(), - ImportExportType::ExportLabels => format!("liana-labels-{date}.csv"), + ImportExportType::ExportLabels => format!("liana-labels-{date}.jsonl"), ImportExportType::ExportBackup(_) => format!("liana-backup-{date}.json"), } } @@ -95,8 +95,12 @@ impl ExportModal { self.state = ImportExportState::Progress(p); } } - Progress::Finished | Progress::Ended => self.state = ImportExportState::Ended, - Progress::Error(e) => self.error = Some(e), + Progress::Finished | Progress::Ended => { + self.state = ImportExportState::Ended; + } + Progress::Error(e) => { + self.error = Some(e); + } Progress::None => {} Progress::Psbt(_) => { if self.import_export_type == ImportExportType::ImportPsbt { diff --git a/liana-gui/src/app/state/settings/mod.rs b/liana-gui/src/app/state/settings/mod.rs index 69b3117d..faede757 100644 --- a/liana-gui/src/app/state/settings/mod.rs +++ b/liana-gui/src/app/state/settings/mod.rs @@ -271,13 +271,15 @@ impl State for ImportExportSettingsState { }, |s| { Message::View(view::Message::Settings( - view::SettingsMessage::ExportBackup(s), + view::SettingsMessage::ExportBackup(Ok(s)), )) }, ); } } - Message::View(view::Message::Settings(view::SettingsMessage::ExportBackup(backup))) => { + Message::View(view::Message::Settings(view::SettingsMessage::ExportBackup(Ok( + backup, + )))) => { let modal = ExportModal::new(Some(daemon), ImportExportType::ExportBackup(backup)); launch!(self, modal); } diff --git a/liana-gui/src/app/state/settings/wallet.rs b/liana-gui/src/app/state/settings/wallet.rs index 1315f373..917f764c 100644 --- a/liana-gui/src/app/state/settings/wallet.rs +++ b/liana-gui/src/app/state/settings/wallet.rs @@ -240,7 +240,7 @@ impl State for WalletSettingsState { }, |s| { Message::View(view::Message::Settings( - view::SettingsMessage::ExportBackup(s), + view::SettingsMessage::ExportBackup(Ok(s)), )) }, ) @@ -248,7 +248,9 @@ impl State for WalletSettingsState { Task::none() } } - Message::View(view::Message::Settings(view::SettingsMessage::ExportBackup(backup))) => { + Message::View(view::Message::Settings(view::SettingsMessage::ExportBackup(Ok( + backup, + )))) => { if self.modal.is_none() { let modal = // ExportModal::new(Some(daemon), ImportExportType::ExportBackup(backup)); diff --git a/liana-gui/src/app/view/message.rs b/liana-gui/src/app/view/message.rs index 6ff27a6c..e5c46436 100644 --- a/liana-gui/src/app/view/message.rs +++ b/liana-gui/src/app/view/message.rs @@ -1,4 +1,4 @@ -use crate::{app::menu::Menu, export::ImportExportMessage, node::bitcoind::RpcAuthType}; +use crate::{app::menu::Menu, backup, export::ImportExportMessage, node::bitcoind::RpcAuthType}; use liana::miniscript::bitcoin::{bip32::Fingerprint, OutPoint}; pub trait Close { @@ -89,7 +89,7 @@ pub enum SettingsMessage { ExportTransactions, ExportLabels, ExportWallet, - ExportBackup(String), + ExportBackup(Result), ImportWallet, AboutSection, RegisterWallet, diff --git a/liana-gui/src/backup.rs b/liana-gui/src/backup.rs index 86c4398d..2c563f9c 100644 --- a/liana-gui/src/backup.rs +++ b/liana-gui/src/backup.rs @@ -46,19 +46,19 @@ pub struct Backup { pub proprietary: serde_json::Map, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum Error { DescriptorMissing, NotSingleWallet, Json, SettingsFromFile, - Daemon(DaemonError), + Daemon(String), TxTimeMissing, } impl From for Error { fn from(value: DaemonError) -> Self { - Error::Daemon(value) + Error::Daemon(value.to_string()) } } diff --git a/liana-gui/src/installer/message.rs b/liana-gui/src/installer/message.rs index 414532ac..81ebc225 100644 --- a/liana-gui/src/installer/message.rs +++ b/liana-gui/src/installer/message.rs @@ -8,6 +8,7 @@ use super::{context, Error}; use crate::{ app::settings::ProviderKey, app::view::Close, + backup, download::{DownloadError, Progress}, export::ImportExportMessage, hw::HardwareWalletMessage, @@ -51,6 +52,8 @@ pub enum Message { RedeemNextKey, KeyRedeemed(ProviderKey, Result<(), services::Error>), AllKeysRedeemed, + BackupWallet, + ExportWallet(Result), ImportExport(ImportExportMessage), } @@ -62,7 +65,7 @@ impl Close for Message { impl From for Message { fn from(value: ImportExportMessage) -> Self { - Self::ImportExport(value) + Message::ImportExport(value) } } diff --git a/liana-gui/src/installer/mod.rs b/liana-gui/src/installer/mod.rs index 4c946087..4c0a6144 100644 --- a/liana-gui/src/installer/mod.rs +++ b/liana-gui/src/installer/mod.rs @@ -25,6 +25,7 @@ use crate::{ settings::{self as gui_settings, AuthConfig, Settings, SettingsError, WalletSetting}, wallet::wallet_name, }, + backup, daemon::DaemonError, datadir::create_directory, hw::{HardwareWalletConfig, HardwareWallets}, @@ -730,6 +731,7 @@ pub enum Error { CannotGetAvailablePort(String), Unexpected(String), HardwareWallet(async_hwi::Error), + Backup(backup::Error), } impl From for Error { @@ -783,6 +785,7 @@ impl std::fmt::Display for Error { Self::CannotCreateFile(e) => write!(f, "Failed to create file: {}", e), Self::Unexpected(e) => write!(f, "Unexpected: {}", e), Self::HardwareWallet(e) => write!(f, "Hardware Wallet: {}", e), + Self::Backup(e) => write!(f, "Backup: {:?}", e), } } } diff --git a/liana-gui/src/installer/step/descriptor/mod.rs b/liana-gui/src/installer/step/descriptor/mod.rs index e98463b9..0bf6d31e 100644 --- a/liana-gui/src/installer/step/descriptor/mod.rs +++ b/liana-gui/src/installer/step/descriptor/mod.rs @@ -16,7 +16,9 @@ use liana_ui::{component::form, widget::Element}; use async_hwi::DeviceKind; use crate::{ - app::{settings::KeySetting, wallet::wallet_name}, + app::{settings::KeySetting, state::export::ExportModal, wallet::wallet_name}, + backup::{self, Backup}, + export::{ImportExportMessage, ImportExportType}, hw::{HardwareWallet, HardwareWallets}, installer::{ message::{self, Message}, @@ -292,16 +294,71 @@ pub struct BackupDescriptor { done: bool, descriptor: Option, keys: HashMap, + modal: Option, + error: Option, + context: Option, } impl Step for BackupDescriptor { + fn subscription(&self, _hws: &HardwareWallets) -> Subscription { + if let Some(modal) = &self.modal { + if let Some(sub) = modal.subscription() { + sub.map(|m| Message::ImportExport(ImportExportMessage::Progress(m))) + } else { + Subscription::none() + } + } else { + Subscription::none() + } + } fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Task { - if let Message::UserActionDone(done) = message { - self.done = done; + match message { + Message::ImportExport(ImportExportMessage::Close) => { + self.modal = None; + } + Message::ImportExport(m) => { + if let Some(modal) = self.modal.as_mut() { + let task: Task = modal.update(m); + return task; + }; + } + Message::BackupWallet => { + if let (None, Some(ctx)) = (&self.modal, self.context.as_ref()) { + let ctx = ctx.clone(); + return Task::perform( + async move { + let backup = Backup::from_installer(ctx, true).await?; + serde_json::to_string_pretty(&backup).map_err(|_| backup::Error::Json) + }, + Message::ExportWallet, + ); + } + } + Message::ExportWallet(str) => { + if self.modal.is_none() { + let str = match str { + Ok(s) => s, + Err(e) => { + tracing::error!("{e:?}"); + self.error = Some(Error::Backup(e)); + return Task::none(); + } + }; + let modal = ExportModal::new(None, ImportExportType::ExportBackup(str)); + let launch = modal.launch(); + self.modal = Some(modal); + return launch; + } + } + Message::UserActionDone(done) => { + self.done = done; + } + _ => {} } Task::none() } fn load_context(&mut self, ctx: &Context) { + self.context = Some(ctx.clone()); if self.descriptor != ctx.descriptor { self.descriptor.clone_from(&ctx.descriptor); self.done = false; @@ -318,13 +375,19 @@ impl Step for BackupDescriptor { progress: (usize, usize), email: Option<&'a str>, ) -> Element { - view::backup_descriptor( + let content = view::backup_descriptor( progress, email, self.descriptor.as_ref().expect("Must be a descriptor"), &self.keys, + self.error.as_ref(), self.done, - ) + ); + if let Some(modal) = &self.modal { + modal.view(content) + } else { + content + } } } diff --git a/liana-gui/src/installer/view/mod.rs b/liana-gui/src/installer/view/mod.rs index c85a542e..733df5b9 100644 --- a/liana-gui/src/installer/view/mod.rs +++ b/liana-gui/src/installer/view/mod.rs @@ -689,6 +689,7 @@ pub fn backup_descriptor<'a>( email: Option<&'a str>, descriptor: &'a LianaDescriptor, keys: &'a HashMap, + error: Option<&Error>, done: bool, ) -> Element<'a, Message> { layout( @@ -724,6 +725,7 @@ pub fn backup_descriptor<'a>( )) .max_width(1000), ) + .push_maybe(error.map(|e| card::error("Failed to export backup", e.to_string()))) .push( card::simple( Column::new() @@ -741,10 +743,17 @@ pub fn backup_descriptor<'a>( ), ) .push( - Row::new().push(Column::new().width(Length::Fill)).push( - button::secondary(Some(icon::clipboard_icon()), "Copy") - .on_press(Message::Clibpboard(descriptor.to_string())), - ), + Row::new() + .push(Space::with_width(Length::Fill)) + .push( + button::secondary(Some(icon::wallet_icon()), "Backup") + .on_press(Message::BackupWallet), + ) + .push(Space::with_width(10)) + .push( + button::secondary(Some(icon::clipboard_icon()), "Copy") + .on_press(Message::Clibpboard(descriptor.to_string())), + ), ) .spacing(10), ) From 61793b35ce4b329690970fc26a6a6f629513f66e Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Sun, 16 Mar 2025 06:20:33 +0100 Subject: [PATCH 16/44] backup: backup coins --- liana-gui/src/backup.rs | 48 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/liana-gui/src/backup.rs b/liana-gui/src/backup.rs index 2c563f9c..89be5659 100644 --- a/liana-gui/src/backup.rs +++ b/liana-gui/src/backup.rs @@ -3,7 +3,10 @@ use liana::miniscript::{ self, bitcoin::{bip32::Fingerprint, Network, Txid}, }; -use lianad::bip329; +use lianad::{ + bip329, + commands::{CoinStatus, ListCoinsEntry}, +}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::{ @@ -213,6 +216,19 @@ impl Backup { .map(|tx| tx.psbt.serialize_hex()) .collect(); + let statuses = [ + CoinStatus::Unconfirmed, + CoinStatus::Confirmed, + CoinStatus::Spending, + ]; + account.coins = daemon + .list_coins(&statuses, &[]) + .await? + .coins + .into_iter() + .map(|c| (c.outpoint.clone().to_string(), Coin::from(c))) + .collect(); + Ok(Backup { name: Some(name), accounts: vec![account], @@ -337,10 +353,39 @@ pub struct Account { pub transactions: Vec, #[serde(skip_serializing_if = "Vec::is_empty")] pub psbts: Vec, + #[serde(skip_serializing_if = "BTreeMap::is_empty")] + pub coins: BTreeMap, #[serde(skip_serializing_if = "serde_json::Map::is_empty")] pub proprietary: serde_json::Map, } +#[derive(Debug, Serialize, Deserialize)] +pub struct Coin { + amount: u64, + outpoint: String, + address: String, + block_height: Option, + account: u32, + derivation_index: u32, + is_coinbase: Option, + is_from_self: Option, +} + +impl From for Coin { + fn from(value: ListCoinsEntry) -> Self { + Self { + amount: value.amount.to_sat(), + outpoint: value.outpoint.to_string(), + address: value.address.to_string(), + block_height: value.block_height, + account: if value.is_change { 1 } else { 0 }, + derivation_index: value.derivation_index.into(), + is_coinbase: if value.is_immature { Some(true) } else { None }, + is_from_self: Some(value.is_from_self), + } + } +} + impl Account { pub fn new(descriptor: String) -> Self { Self { @@ -353,6 +398,7 @@ impl Account { labels: None, transactions: Vec::new(), psbts: Vec::new(), + coins: BTreeMap::new(), proprietary: serde_json::Map::new(), } } From c4585e8efe8f0ff27b92b8e930582650b6460e9f Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Sun, 16 Mar 2025 06:56:43 +0100 Subject: [PATCH 17/44] backup: add backup & Liana versions --- liana-gui/src/backup.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/liana-gui/src/backup.rs b/liana-gui/src/backup.rs index 89be5659..d9a247eb 100644 --- a/liana-gui/src/backup.rs +++ b/liana-gui/src/backup.rs @@ -25,10 +25,16 @@ use crate::{ RemoteBackend, }, lianalite::client::backend::api::DEFAULT_LIMIT, + VERSION, }; const CONFIG_KEY: &str = "config"; const SETTINGS_KEY: &str = "settings"; +const LIANA_VERSION_KEY: &str = "liana_version"; + +pub fn liana_version() -> String { + format!("{}.{}.{}", VERSION.major, VERSION.minor, VERSION.patch) +} fn now() -> u64 { SystemTime::now() @@ -47,6 +53,12 @@ pub struct Backup { /// App proprietary metadata (settings, configuration, etc..) #[serde(skip_serializing_if = "serde_json::Map::is_empty")] pub proprietary: serde_json::Map, + #[serde(default = "default_version")] + pub version: u32, +} + +fn default_version() -> u32 { + 0 } #[derive(Debug, Clone)] @@ -98,6 +110,7 @@ impl Backup { let mut account = Account::new(descriptor); let mut proprietary = serde_json::Map::new(); + proprietary.insert(LIANA_VERSION_KEY.to_string(), liana_version().into()); let config = extract_daemon_config(&ctx); if let Ok(config) = serde_json::to_value(config) { @@ -145,6 +158,7 @@ impl Backup { network: ctx.network, proprietary: serde_json::Map::new(), date: now, + version: 0, }) } @@ -157,6 +171,8 @@ impl Backup { daemon: Arc, ) -> Result { let mut proprietary = serde_json::Map::new(); + proprietary.insert(LIANA_VERSION_KEY.to_string(), liana_version().into()); + let name = wallet.name.clone(); let descriptor = wallet.main_descriptor.to_string(); let keys = wallet.keys(); @@ -235,6 +251,7 @@ impl Backup { network, proprietary: serde_json::Map::new(), date: now(), + version: 0, }) } From 325e2dc9b91c4553384c4751cdcfe9bee57f5a33 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Sun, 16 Mar 2025 07:30:06 +0100 Subject: [PATCH 18/44] backup: add chain tip --- liana-gui/src/backup.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/liana-gui/src/backup.rs b/liana-gui/src/backup.rs index d9a247eb..94e3d3fd 100644 --- a/liana-gui/src/backup.rs +++ b/liana-gui/src/backup.rs @@ -189,10 +189,16 @@ impl Backup { proprietary.insert(CONFIG_KEY.to_string(), config); } + let info = daemon.get_info().await?; + let mut account = Account::new(descriptor); + + account.chain_tip = Some(ChainTip { + block_height: info.block_height, + block_hash: None, + }); account.proprietary = proprietary; account.name = Some(name.clone()); - let info = daemon.get_info().await?; account.timestamp = Some(info.timestamp as u64); account.change_index = Some(info.change_index); account.receive_index = Some(info.receive_index); @@ -372,11 +378,19 @@ pub struct Account { pub psbts: Vec, #[serde(skip_serializing_if = "BTreeMap::is_empty")] pub coins: BTreeMap, + #[serde(skip_serializing_if = "Option::is_none")] + pub chain_tip: Option, #[serde(skip_serializing_if = "serde_json::Map::is_empty")] pub proprietary: serde_json::Map, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ChainTip { + pub block_height: i32, + pub block_hash: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct Coin { amount: u64, outpoint: String, @@ -417,6 +431,7 @@ impl Account { psbts: Vec::new(), coins: BTreeMap::new(), proprietary: serde_json::Map::new(), + chain_tip: None, } } } From 4ba6486645766b65d12be063b8e1ab644b7c45bb Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Mon, 10 Mar 2025 14:35:55 +0100 Subject: [PATCH 19/44] gui: add import/export features in settings --- liana-gui/src/app/state/settings/wallet.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/liana-gui/src/app/state/settings/wallet.rs b/liana-gui/src/app/state/settings/wallet.rs index 917f764c..b1d65157 100644 --- a/liana-gui/src/app/state/settings/wallet.rs +++ b/liana-gui/src/app/state/settings/wallet.rs @@ -253,7 +253,6 @@ impl State for WalletSettingsState { )))) => { if self.modal.is_none() { let modal = - // ExportModal::new(Some(daemon), ImportExportType::ExportBackup(backup)); ExportModal::new(Some(daemon), ImportExportType::ExportBackup(backup)); let launch = modal.launch(); self.modal = Modal::ImportExport(modal); @@ -265,7 +264,6 @@ impl State for WalletSettingsState { Message::View(view::Message::Settings(view::SettingsMessage::ImportWallet)) => { if self.modal.is_none() { let modal = ExportModal::new( - // Some(daemon), Some(daemon), ImportExportType::Descriptor(self.wallet.main_descriptor.clone()), ); From 31f834f41dd355116dd039b05541f24d798b1fe3 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Wed, 12 Mar 2025 17:49:03 +0100 Subject: [PATCH 20/44] backup: default deserialization --- liana-gui/src/backup.rs | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/liana-gui/src/backup.rs b/liana-gui/src/backup.rs index 94e3d3fd..df4894f9 100644 --- a/liana-gui/src/backup.rs +++ b/liana-gui/src/backup.rs @@ -45,13 +45,14 @@ fn now() -> u64 { #[derive(Serialize, Deserialize)] pub struct Backup { - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub name: Option, + #[serde(default)] pub accounts: Vec, pub network: Network, pub date: u64, /// App proprietary metadata (settings, configuration, etc..) - #[serde(skip_serializing_if = "serde_json::Map::is_empty")] + #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")] pub proprietary: serde_json::Map, #[serde(default = "default_version")] pub version: u32, @@ -359,28 +360,28 @@ async fn get_transactions( #[derive(Debug, Serialize, Deserialize)] pub struct Account { - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub name: Option, pub descriptor: String, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub receive_index: Option, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub change_index: Option, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub timestamp: Option, - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub keys: BTreeMap, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub labels: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub transactions: Vec, - #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub psbts: Vec, - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub coins: BTreeMap, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub chain_tip: Option, - #[serde(skip_serializing_if = "serde_json::Map::is_empty")] + #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")] pub proprietary: serde_json::Map, } From 4c2adec67c09befc604e7dd70d5a19e5fae6adf4 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Thu, 13 Mar 2025 06:46:01 +0100 Subject: [PATCH 21/44] backup: serialize PSBT w/ Psbt::to_string() instead Psbt::serialize_hex() --- liana-gui/src/backup.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/liana-gui/src/backup.rs b/liana-gui/src/backup.rs index df4894f9..ddea022e 100644 --- a/liana-gui/src/backup.rs +++ b/liana-gui/src/backup.rs @@ -236,7 +236,7 @@ impl Backup { .list_spend_transactions(None) .await? .into_iter() - .map(|tx| tx.psbt.serialize_hex()) + .map(|tx| tx.psbt.to_string()) .collect(); let statuses = [ From 39f71d3de8570e776096f2ce904cc1d783d96703 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Sat, 8 Mar 2025 12:54:46 +0100 Subject: [PATCH 22/44] lianad: add update_deriv_indexes() command --- lianad/src/commands/mod.rs | 39 +++++++++++++++++++++++++++++- lianad/src/jsonrpc/api.rs | 49 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index ff0e223e..1c9dabde 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -36,7 +36,11 @@ use std::{ }; use miniscript::{ - bitcoin::{self, address, bip32, psbt::Psbt}, + bitcoin::{ + self, address, + bip32::{self, ChildNumber}, + psbt::Psbt, + }, psbt::PsbtExt, }; use serde::{Deserialize, Serialize}; @@ -353,6 +357,39 @@ impl DaemonControl { GetAddressResult::new(address, index) } + /// Update derivation indexes + pub fn update_deriv_indexes( + &self, + receive: Option, + change: Option, + ) -> Result<(), CommandError> { + let mut db_conn = self.db.connection(); + + if let Some(index) = receive { + let child = match ChildNumber::from_normal_idx(index) { + Ok(i) => i, + Err(_) => return Err(CommandError::InvalidDerivationIndex), + }; + let db_receive = db_conn.receive_index(); + if child > db_receive { + db_conn.set_receive_index(child, &self.secp); + } + } + + if let Some(index) = change { + let child = match ChildNumber::from_normal_idx(index) { + Ok(i) => i, + Err(_) => return Err(CommandError::InvalidDerivationIndex), + }; + let db_change = db_conn.change_index(); + if child > db_change { + db_conn.set_change_index(child, &self.secp); + } + } + + Ok(()) + } + /// list addresses pub fn list_addresses( &self, diff --git a/lianad/src/jsonrpc/api.rs b/lianad/src/jsonrpc/api.rs index fe087a93..29a0ce85 100644 --- a/lianad/src/jsonrpc/api.rs +++ b/lianad/src/jsonrpc/api.rs @@ -199,6 +199,49 @@ fn list_addresses( Ok(serde_json::json!(&res)) } +fn update_deriv_indexes( + control: &DaemonControl, + params: Params, +) -> Result { + let receive = params.get(0, "receive"); + let change = params.get(1, "change"); + + if receive.is_none() && change.is_none() { + return Err(Error::invalid_params( + "Missing 'receive' or 'change' parameter", + )); + } + + let receive = match receive { + Some(i) => { + let res = i.as_i64().ok_or(Error::invalid_params( + "Invalid value for 'receive' param".to_string(), + ))?; + let res = res + .try_into() + .map_err(|_| Error::invalid_params("Invalid value for 'receive' param"))?; + Some(res) + } + None => None, + }; + + let change = match change { + Some(i) => { + let res = i.as_i64().ok_or(Error::invalid_params( + "Invalid value for 'change' param".to_string(), + ))?; + let res = res + .try_into() + .map_err(|_| Error::invalid_params("Invalid value for 'change' param"))?; + Some(res) + } + None => None, + }; + + control.update_deriv_indexes(receive, change)?; + Ok(serde_json::json!({})) +} + fn list_confirmed(control: &DaemonControl, params: Params) -> Result { let start: u32 = params .get(0, "start") @@ -417,6 +460,12 @@ pub fn handle_request(control: &mut DaemonControl, req: Request) -> Result serde_json::json!(&control.get_info()), "getnewaddress" => serde_json::json!(&control.get_new_address()), + "updatederivationindexes" => { + let params = req.params.ok_or_else(|| { + Error::invalid_params("Missing 'receive' or 'change' parameters.") + })?; + update_deriv_indexes(control, params)? + } "listcoins" => { let params = req.params; list_coins(control, params)? From 6988fa40c92d2b2c518456284d685810c1ffb575 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Sat, 8 Mar 2025 12:55:57 +0100 Subject: [PATCH 23/44] gui: implement Daemon.update_deriv_indexes() --- liana-gui/src/app/error.rs | 1 + liana-gui/src/app/view/warning.rs | 3 +++ liana-gui/src/daemon/client/mod.rs | 8 ++++++++ liana-gui/src/daemon/embedded.rs | 13 +++++++++++++ liana-gui/src/daemon/mod.rs | 8 ++++++++ liana-gui/src/lianalite/client/backend/mod.rs | 8 ++++++++ 6 files changed, 41 insertions(+) diff --git a/liana-gui/src/app/error.rs b/liana-gui/src/app/error.rs index 4f279242..4050ed88 100644 --- a/liana-gui/src/app/error.rs +++ b/liana-gui/src/app/error.rs @@ -53,6 +53,7 @@ impl std::fmt::Display for Error { write!(f, "[{:?}] {}", code, e) } DaemonError::CoinSelectionError => write!(f, "{}", e), + DaemonError::NotImplemented => write!(f, "{}", e), }, Self::Unexpected(e) => write!(f, "Unexpected error: {}", e), Self::HardwareWallet(e) => write!(f, "error: {}\nPlease check if the device is still connected and unlocked with the correct firmware open for the current network and no other application is accessing the device.", e), diff --git a/liana-gui/src/app/view/warning.rs b/liana-gui/src/app/view/warning.rs index 1496646a..cdc18bd8 100644 --- a/liana-gui/src/app/view/warning.rs +++ b/liana-gui/src/app/view/warning.rs @@ -41,6 +41,9 @@ impl From<&Error> for WarningMessage { DaemonError::CoinSelectionError => { WarningMessage("Error when selecting coins for spend".to_string()) } + DaemonError::NotImplemented => { + WarningMessage("Feature not implemented for this backend".to_string()) + } }, Error::Unexpected(_) => WarningMessage("Unknown error".to_string()), Error::HardwareWallet(_) => WarningMessage("Hardware wallet error".to_string()), diff --git a/liana-gui/src/daemon/client/mod.rs b/liana-gui/src/daemon/client/mod.rs index 80ba9176..c0f69263 100644 --- a/liana-gui/src/daemon/client/mod.rs +++ b/liana-gui/src/daemon/client/mod.rs @@ -83,6 +83,14 @@ impl Daemon for Lianad { self.call("getnewaddress", Option::::None) } + async fn update_deriv_indexes( + &self, + receive: Option, + change: Option, + ) -> Result<(), DaemonError> { + self.call("updatederivationindexes", Some(vec![receive, change])) + } + async fn list_coins( &self, statuses: &[CoinStatus], diff --git a/liana-gui/src/daemon/embedded.rs b/liana-gui/src/daemon/embedded.rs index 4cafd5cc..96e63613 100644 --- a/liana-gui/src/daemon/embedded.rs +++ b/liana-gui/src/daemon/embedded.rs @@ -98,6 +98,19 @@ impl Daemon for EmbeddedDaemon { self.command(|daemon| Ok(daemon.get_new_address())).await } + async fn update_deriv_indexes( + &self, + receive: Option, + change: Option, + ) -> Result<(), DaemonError> { + self.command(|daemon| { + daemon + .update_deriv_indexes(receive, change) + .map_err(|e| DaemonError::Unexpected(e.to_string())) + }) + .await + } + async fn list_coins( &self, statuses: &[CoinStatus], diff --git a/liana-gui/src/daemon/mod.rs b/liana-gui/src/daemon/mod.rs index f8779267..300fc918 100644 --- a/liana-gui/src/daemon/mod.rs +++ b/liana-gui/src/daemon/mod.rs @@ -43,6 +43,8 @@ pub enum DaemonError { ClientNotSupported, /// Error when selecting coins for spend. CoinSelectionError, + /// Not implemented feature + NotImplemented, } impl std::fmt::Display for DaemonError { @@ -57,6 +59,7 @@ impl std::fmt::Display for DaemonError { Self::Start(e) => write!(f, "Daemon did not start: {}", e), Self::ClientNotSupported => write!(f, "Daemon communication is not supported"), Self::CoinSelectionError => write!(f, "Coin selection error"), + Self::NotImplemented => write!(f, "This feature is not implemented for this backend"), } } } @@ -82,6 +85,11 @@ pub trait Daemon: Debug { async fn stop(&self) -> Result<(), DaemonError>; async fn get_info(&self) -> Result; async fn get_new_address(&self) -> Result; + async fn update_deriv_indexes( + &self, + receive: Option, + change: Option, + ) -> Result<(), DaemonError>; async fn list_coins( &self, statuses: &[CoinStatus], diff --git a/liana-gui/src/lianalite/client/backend/mod.rs b/liana-gui/src/lianalite/client/backend/mod.rs index 62d74019..e6095090 100644 --- a/liana-gui/src/lianalite/client/backend/mod.rs +++ b/liana-gui/src/lianalite/client/backend/mod.rs @@ -627,6 +627,14 @@ impl Daemon for BackendWalletClient { }) } + async fn update_deriv_indexes( + &self, + _receive: Option, + _change: Option, + ) -> Result<(), DaemonError> { + Err(DaemonError::NotImplemented) + } + /// Spent coins are not returned if statuses is empty, unless their outpoints are specified. async fn list_coins( &self, From 02e28bbd3e72ae5aacde38552ac5dc892639a20e Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Sat, 8 Mar 2025 19:10:51 +0100 Subject: [PATCH 24/44] gui: implement Daemon.update_wallet_metadata() for lianad --- liana-gui/src/app/settings.rs | 19 +++++++++++++++++++ liana-gui/src/daemon/mod.rs | 26 ++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/liana-gui/src/app/settings.rs b/liana-gui/src/app/settings.rs index 27471b5b..7657de98 100644 --- a/liana-gui/src/app/settings.rs +++ b/liana-gui/src/app/settings.rs @@ -107,6 +107,25 @@ impl WalletSetting { } map } + + pub fn update_alias(&mut self, key: &Fingerprint, alias: &str) { + let key_aliases = self.keys_aliases(); + if key_aliases.contains_key(key) { + self.keys = self + .keys + .clone() + .into_iter() + .map(|mut ks| { + if ks.master_fingerprint == *key { + ks.name = alias.into(); + ks + } else { + ks + } + }) + .collect(); + } + } } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] diff --git a/liana-gui/src/daemon/mod.rs b/liana-gui/src/daemon/mod.rs index 300fc918..e2706271 100644 --- a/liana-gui/src/daemon/mod.rs +++ b/liana-gui/src/daemon/mod.rs @@ -21,6 +21,7 @@ use lianad::{ StartupError, }; +use crate::app::settings::Settings; use crate::{hw::HardwareWalletConfig, node}; #[derive(Debug)] @@ -364,12 +365,33 @@ pub trait Daemon: Debug { Ok(events) } - /// Implemented by LianaLite backend + /// Reimplemented by LianaLite backend async fn update_wallet_metadata( &self, - _fingerprint_aliases: &HashMap, + fingerprint_aliases: &HashMap, _hws: &[HardwareWalletConfig], ) -> Result<(), DaemonError> { + if let Some(datadir) = self + .config() + .ok_or(DaemonError::Unexpected("Config missing".into()))? + .data_dir() + { + let network = self.get_info().await?.network; + let mut settings = Settings::from_file(datadir, network) + .map_err(|_| DaemonError::Unexpected("Fail to read Settings from file".into()))?; + let wallet = if settings.wallets.len() == 1 { + settings.wallets.get_mut(0).expect("already checked") + } else { + return Err(DaemonError::Unexpected( + "Settings file contains more than one wallet".into(), + )); + }; + for fg in wallet.keys_aliases().keys() { + if fingerprint_aliases.contains_key(fg) { + wallet.update_alias(fg, fingerprint_aliases.get(fg).expect("checked")); + } + } + } Ok(()) } } From b6f900355c6803490a79c0c058252ca5a45afb0c Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Sun, 9 Mar 2025 05:15:48 +0100 Subject: [PATCH 25/44] lianad: implement LabelItem.from_bip329() --- lianad/src/database/mod.rs | 42 +++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/lianad/src/database/mod.rs b/lianad/src/database/mod.rs index d88c598b..4265364a 100644 --- a/lianad/src/database/mod.rs +++ b/lianad/src/database/mod.rs @@ -21,7 +21,7 @@ use std::{ }; use bip329::Labels; -use miniscript::bitcoin::{self, bip32, psbt::Psbt, secp256k1}; +use miniscript::bitcoin::{self, bip32, psbt::Psbt, secp256k1, Address, Network, OutPoint, Txid}; /// Information about the wallet. /// @@ -571,6 +571,46 @@ impl LabelItem { None } } + + pub fn from_bip329(label: &bip329::Label, network: Network) -> Option<(Self, String)> { + match label { + bip329::Label::Transaction(tx_record) => { + if let (Some(txid), Some(label)) = ( + Txid::from_str(&tx_record.ref_).ok(), + tx_record.label.clone(), + ) { + Some((Self::Txid(txid), label)) + } else { + None + } + } + bip329::Label::Address(address_record) => { + if let (Some(addr), Some(label)) = ( + Address::from_str(&address_record.ref_).ok(), + address_record.label.clone(), + ) { + if addr.is_valid_for_network(network) { + Some((Self::Address(addr.assume_checked()), label)) + } else { + None + } + } else { + None + } + } + bip329::Label::Output(output_record) => { + if let (Some(outpoint), Some(label)) = ( + OutPoint::from_str(&output_record.ref_).ok(), + output_record.label.clone(), + ) { + Some((Self::OutPoint(outpoint), label)) + } else { + None + } + } + _ => None, + } + } } #[cfg(test)] From d0d7007e8398035e43d6d32eff149f167301ae32 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Sat, 8 Mar 2025 10:54:41 +0100 Subject: [PATCH 26/44] import: implement import_backup() --- Cargo.lock | 481 ++++++++++++++++-- liana-gui/src/app/state/export.rs | 62 ++- liana-gui/src/app/state/settings/mod.rs | 12 +- liana-gui/src/app/state/settings/wallet.rs | 4 +- liana-gui/src/app/state/transactions.rs | 2 +- liana-gui/src/app/view/export.rs | 65 ++- liana-gui/src/backup.rs | 12 +- liana-gui/src/export.rs | 433 +++++++++++++++- .../src/installer/step/descriptor/mod.rs | 2 +- lianad/Cargo.toml | 4 +- lianad/src/database/mod.rs | 7 +- lianad/src/database/sqlite/schema.rs | 24 +- 12 files changed, 1018 insertions(+), 90 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 79b4137f..ba52bb17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,6 +68,49 @@ dependencies = [ "subtle", ] +[[package]] +name = "age" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fc171f4874fa10887e47088f81a55fcf030cd421aa31ec2b370cafebcc608a" +dependencies = [ + "age-core", + "base64 0.21.7", + "bech32 0.9.1", + "chacha20poly1305", + "cookie-factory", + "hmac", + "i18n-embed", + "i18n-embed-fl", + "lazy_static", + "nom", + "pin-project", + "rand", + "rust-embed", + "scrypt", + "sha2", + "subtle", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "age-core" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2bf6a89c984ca9d850913ece2da39e1d200563b0a94b002b253beee4c5acf99" +dependencies = [ + "base64 0.21.7", + "chacha20poly1305", + "cookie-factory", + "hkdf", + "io_tee", + "nom", + "rand", + "secrecy", + "sha2", +] + [[package]] name = "ahash" version = "0.8.11" @@ -120,7 +163,7 @@ dependencies = [ "ndk-context", "ndk-sys 0.6.0+11769913", "num_enum", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -168,6 +211,12 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "arrayref" version = "0.3.9" @@ -484,6 +533,15 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + [[package]] name = "bdk_chain" version = "0.16.0" @@ -508,6 +566,12 @@ dependencies = [ "electrum-client", ] +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + [[package]] name = "bech32" version = "0.11.0" @@ -516,13 +580,16 @@ checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" [[package]] name = "bip329" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdacd3f71820072c8d76eb077a8177b9a001bf03723d797783f97e9feefc0515" +checksum = "8351c1cf438ae5814ad2a696f06fd07d7be4a1a133d889b1785260fa8797798c" dependencies = [ + "age", + "bitcoin", + "hex", "serde", "serde_json", - "thiserror", + "thiserror 2.0.12", ] [[package]] @@ -579,7 +646,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", "tokio", "zeroize", ] @@ -592,7 +659,7 @@ checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026" dependencies = [ "base58ck", "base64 0.21.7", - "bech32", + "bech32 0.11.0", "bitcoin-internals 0.3.0", "bitcoin-io", "bitcoin-units", @@ -804,7 +871,7 @@ dependencies = [ "polling", "rustix", "slab", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -938,7 +1005,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4274ea815e013e0f9f04a2633423e14194e408a0576c943ce3d14ca56c50031c" dependencies = [ - "thiserror", + "thiserror 1.0.69", "x11rb", ] @@ -1049,6 +1116,15 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" +dependencies = [ + "futures", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1136,7 +1212,7 @@ dependencies = [ "rayon", "rustc-hash 1.1.0", "rustybuzz", - "self_cell", + "self_cell 1.1.0", "swash", "sys-locale", "ttf-parser 0.21.1", @@ -1275,6 +1351,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core 0.9.10", +] + [[package]] name = "data-url" version = "0.3.1" @@ -1695,6 +1785,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "find-crate" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" +dependencies = [ + "toml", +] + [[package]] name = "fixedbitset" version = "0.4.2" @@ -1723,6 +1822,50 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" +[[package]] +name = "fluent" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb74634707bebd0ce645a981148e8fb8c7bccd4c33c652aeffd28bf2f96d555a" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe0a21ee80050c678013f82edf4b705fe2f26f1f9877593d13198612503f493" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash 1.1.0", + "self_cell 0.10.3", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d" +dependencies = [ + "thiserror 1.0.69", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2049,7 +2192,7 @@ checksum = "6f56f6318968d03c18e1bcf4857ff88c61157e9da8e47c5f29055d60e1228884" dependencies = [ "log", "presser", - "thiserror", + "thiserror 1.0.69", "winapi", "windows", ] @@ -2171,7 +2314,7 @@ dependencies = [ "com", "libc", "libloading 0.8.6", - "thiserror", + "thiserror 1.0.69", "widestring", "winapi", ] @@ -2240,6 +2383,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -2330,6 +2482,73 @@ dependencies = [ "tokio-rustls", ] +[[package]] +name = "i18n-config" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e88074831c0be5b89181b05e6748c4915f77769ecc9a4c372f88b169a8509c9" +dependencies = [ + "basic-toml", + "log", + "serde", + "serde_derive", + "thiserror 1.0.69", + "unic-langid", +] + +[[package]] +name = "i18n-embed" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0454970a5853f498e686cbd7bf9391aac2244928194780cb7a0af0f41937db6" +dependencies = [ + "arc-swap", + "fluent", + "fluent-langneg", + "fluent-syntax", + "i18n-embed-impl", + "intl-memoizer", + "log", + "parking_lot 0.12.3", + "rust-embed", + "thiserror 1.0.69", + "unic-langid", + "walkdir", +] + +[[package]] +name = "i18n-embed-fl" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7578cee2940492a648bd60fb49ca85ee8c821a63790e0ef5b604cfed353b2a" +dependencies = [ + "dashmap", + "find-crate", + "fluent", + "fluent-syntax", + "i18n-config", + "i18n-embed", + "proc-macro-error2", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.98", + "unic-langid", +] + +[[package]] +name = "i18n-embed-impl" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2" +dependencies = [ + "find-crate", + "i18n-config", + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "iana-time-zone" version = "0.1.61" @@ -2365,7 +2584,7 @@ dependencies = [ "iced_widget", "iced_winit", "image", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2383,7 +2602,7 @@ dependencies = [ "palette", "rustc-hash 2.1.1", "smol_str", - "thiserror", + "thiserror 1.0.69", "web-time", ] @@ -2434,7 +2653,7 @@ dependencies = [ "once_cell", "raw-window-handle", "rustc-hash 2.1.1", - "thiserror", + "thiserror 1.0.69", "unicode-segmentation", ] @@ -2448,7 +2667,7 @@ dependencies = [ "iced_tiny_skia", "iced_wgpu", "log", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2461,7 +2680,7 @@ dependencies = [ "iced_core", "iced_futures", "raw-window-handle", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2499,7 +2718,7 @@ dependencies = [ "once_cell", "resvg", "rustc-hash 2.1.1", - "thiserror", + "thiserror 1.0.69", "wgpu", ] @@ -2516,7 +2735,7 @@ dependencies = [ "ouroboros", "qrcode", "rustc-hash 2.1.1", - "thiserror", + "thiserror 1.0.69", "unicode-segmentation", ] @@ -2531,7 +2750,7 @@ dependencies = [ "iced_runtime", "log", "rustc-hash 2.1.1", - "thiserror", + "thiserror 1.0.69", "tracing", "wasm-bindgen-futures", "web-sys", @@ -2731,6 +2950,25 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "intl-memoizer" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe22e020fce238ae18a6d5d8c502ee76a52a6e880d99477657e6acc30ec57bda" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + [[package]] name = "io-kit-sys" version = "0.4.1" @@ -2741,6 +2979,12 @@ dependencies = [ "mach2", ] +[[package]] +name = "io_tee" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304" + [[package]] name = "ipnet" version = "2.11.0" @@ -2782,7 +3026,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] @@ -2951,7 +3195,7 @@ dependencies = [ "ledger-transport", "libc", "log", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -3292,13 +3536,19 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniscript" version = "12.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bd3c9608217b0d6fa9c9c8ddd875b85ab72bd4311cfc8db35e1b5a08fc11f4d" dependencies = [ - "bech32", + "bech32 0.11.0", "bitcoin", "serde", ] @@ -3377,7 +3627,7 @@ dependencies = [ "rustc-hash 1.1.0", "spirv", "termcolor", - "thiserror", + "thiserror 1.0.69", "unicode-xid", ] @@ -3393,7 +3643,7 @@ dependencies = [ "ndk-sys 0.6.0+11769913", "num_enum", "raw-window-handle", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -3474,6 +3724,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -3945,6 +4205,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -4159,6 +4429,28 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "proc-macro2" version = "1.0.93" @@ -4419,7 +4711,7 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -4594,6 +4886,40 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust-embed" +version = "8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b3aba5104622db5c9fc61098de54708feb732e7763d7faa2fa625899f00bf6f" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f198c73be048d2c5aa8e12f7960ad08443e56fd39cc26336719fdb4ea0ebaae" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.98", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a2fcdc9f40c8dc2922842ca9add611ad19f332227fc651d015881ad1552bd9a" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "rust-ini" version = "0.19.0" @@ -4736,6 +5062,15 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "same-file" version = "1.0.6" @@ -4757,6 +5092,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2", + "salsa20", + "sha2", +] + [[package]] name = "sct" version = "0.7.1" @@ -4814,6 +5160,24 @@ dependencies = [ "cc", ] +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "self_cell" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d" +dependencies = [ + "self_cell 1.1.0", +] + [[package]] name = "self_cell" version = "1.1.0" @@ -5033,7 +5397,7 @@ dependencies = [ "log", "memmap2", "rustix", - "thiserror", + "thiserror 1.0.69", "wayland-backend", "wayland-client", "wayland-csd-frame", @@ -5174,6 +5538,12 @@ dependencies = [ "float-cmp", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -5315,7 +5685,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -5329,6 +5708,17 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -5601,6 +5991,15 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +[[package]] +name = "type-map" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f" +dependencies = [ + "rustc-hash 1.1.0", +] + [[package]] name = "typenum" version = "1.17.0" @@ -5624,7 +6023,26 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c878a167baa8afd137494101a688ef8c67125089ff2249284bd2b5f9bfedb815" dependencies = [ - "thiserror", + "thiserror 1.0.69", +] + +[[package]] +name = "unic-langid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dd9d1e72a73b25e07123a80776aae3e7b0ec461ef94f9151eed6ec88005a44" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5422c1f65949306c99240b81de9f3f15929f5a8bfe05bb44b034cc8bf593e5" +dependencies = [ + "serde", + "tinystr", ] [[package]] @@ -6112,7 +6530,7 @@ dependencies = [ "raw-window-handle", "rustc-hash 1.1.0", "smallvec", - "thiserror", + "thiserror 1.0.69", "web-sys", "wgpu-hal", "wgpu-types", @@ -6156,7 +6574,7 @@ dependencies = [ "renderdoc-sys", "rustc-hash 1.1.0", "smallvec", - "thiserror", + "thiserror 1.0.69", "wasm-bindgen", "web-sys", "wgpu-types", @@ -6234,7 +6652,7 @@ dependencies = [ "clipboard_wayland", "clipboard_x11", "raw-window-handle", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -6602,6 +7020,7 @@ checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ "curve25519-dalek", "rand_core", + "serde", "zeroize", ] diff --git a/liana-gui/src/app/state/export.rs b/liana-gui/src/app/state/export.rs index e6f3ac0c..1e413705 100644 --- a/liana-gui/src/app/state/export.rs +++ b/liana-gui/src/app/state/export.rs @@ -48,6 +48,7 @@ impl ExportModal { ImportExportType::ExportLabels => "Export Labels", ImportExportType::ImportPsbt => "Import PSBT", ImportExportType::ImportDescriptor => "Import Descriptor", + ImportExportType::ImportBackup(..) => "Restore Backup", } } @@ -70,12 +71,14 @@ impl ExportModal { ImportExportType::ImportPsbt => "psbt.psbt".into(), ImportExportType::ImportDescriptor => "descriptor.descriptor".into(), ImportExportType::ExportLabels => format!("liana-labels-{date}.jsonl"), - ImportExportType::ExportBackup(_) => format!("liana-backup-{date}.json"), + ImportExportType::ExportBackup(_) | ImportExportType::ImportBackup(_, _) => { + format!("liana-backup-{date}.json") + } } } - pub fn launch + Send + 'static>(&self) -> Task { - Task::perform(get_path(self.default_filename()), move |m| { + pub fn launch + Send + 'static>(&self, write: bool) -> Task { + Task::perform(get_path(self.default_filename(), write), move |m| { ImportExportMessage::Path(m).into() }) } @@ -95,11 +98,21 @@ impl ExportModal { self.state = ImportExportState::Progress(p); } } - Progress::Finished | Progress::Ended => { - self.state = ImportExportState::Ended; + Progress::Finished | Progress::Ended => self.state = ImportExportState::Ended, + Progress::KeyAliasesConflict(ref sender) => { + if let ImportExportType::ImportBackup(_, aliases) = &self.import_export_type { + self.import_export_type = + ImportExportType::ImportBackup(Some(sender.clone()), aliases.clone()); + } + } + Progress::LabelsConflict(ref sender) => { + if let ImportExportType::ImportBackup(labels, _) = &self.import_export_type { + self.import_export_type = + ImportExportType::ImportBackup(labels.clone(), Some(sender.clone())); + } } Progress::Error(e) => { - self.error = Some(e); + self.error = Some(e.clone()); } Progress::None => {} Progress::Psbt(_) => { @@ -130,6 +143,36 @@ impl ExportModal { } } ImportExportMessage::Close | ImportExportMessage::Open => { /* unreachable */ } + ImportExportMessage::Overwrite => { + if let ImportExportType::ImportBackup(labels, aliases) = + &mut self.import_export_type + { + if let Some(sender) = labels.take() { + if sender.send(true).is_err() { + tracing::error!("ExportModal.update(): fail to send labels ACK"); + } + } else if let Some(sender) = aliases.take() { + if sender.send(true).is_err() { + tracing::error!("ExportModal.update(): fail to send aliases ACK"); + } + } + } + } + ImportExportMessage::Ignore => { + if let ImportExportType::ImportBackup(labels, aliases) = + &mut self.import_export_type + { + if let Some(sender) = labels.take() { + if sender.send(false).is_err() { + tracing::error!("ExportModal.update(): fail to send labels NACK"); + } + } else if let Some(sender) = aliases.take() { + if sender.send(false).is_err() { + tracing::error!("ExportModal.update(): fail to send aliases NACK"); + } + } + } + } } Task::none() } @@ -140,7 +183,12 @@ impl ExportModal { { let modal = Modal::new( content, - export_modal(&self.state, self.error.as_ref(), self.modal_title()), + export_modal( + &self.state, + self.error.as_ref(), + self.modal_title(), + &self.import_export_type, + ), ); match self.state { ImportExportState::TimedOut diff --git a/liana-gui/src/app/state/settings/mod.rs b/liana-gui/src/app/state/settings/mod.rs index faede757..d1bc6a0a 100644 --- a/liana-gui/src/app/state/settings/mod.rs +++ b/liana-gui/src/app/state/settings/mod.rs @@ -183,8 +183,8 @@ impl ImportExportSettingsState { } macro_rules! launch { - ($s:ident, $m: ident) => { - let launch = $m.launch(); + ($s:ident, $m: ident, $write:ident) => { + let launch = $m.launch($write); $s.modal = Some($m); return launch }; @@ -239,19 +239,19 @@ impl State for ImportExportSettingsState { Some(daemon), ImportExportType::Descriptor(self.wallet.main_descriptor.clone()), ); - launch!(self, modal); + launch!(self, modal, true); } } Message::View(view::Message::Settings(view::SettingsMessage::ExportTransactions)) => { if self.modal.is_none() { let modal = ExportModal::new(Some(daemon), ImportExportType::Transactions); - launch!(self, modal); + launch!(self, modal, true); } } Message::View(view::Message::Settings(view::SettingsMessage::ExportLabels)) => { if self.modal.is_none() { let modal = ExportModal::new(Some(daemon), ImportExportType::ExportLabels); - launch!(self, modal); + launch!(self, modal, true); } } Message::View(view::Message::Settings(view::SettingsMessage::ExportWallet)) => { @@ -281,7 +281,7 @@ impl State for ImportExportSettingsState { backup, )))) => { let modal = ExportModal::new(Some(daemon), ImportExportType::ExportBackup(backup)); - launch!(self, modal); + launch!(self, modal, true); } Message::View(view::Message::Settings(view::SettingsMessage::ImportWallet)) => { // TODO: diff --git a/liana-gui/src/app/state/settings/wallet.rs b/liana-gui/src/app/state/settings/wallet.rs index b1d65157..dd06a9c5 100644 --- a/liana-gui/src/app/state/settings/wallet.rs +++ b/liana-gui/src/app/state/settings/wallet.rs @@ -254,7 +254,7 @@ impl State for WalletSettingsState { if self.modal.is_none() { let modal = ExportModal::new(Some(daemon), ImportExportType::ExportBackup(backup)); - let launch = modal.launch(); + let launch = modal.launch(true); self.modal = Modal::ImportExport(modal); launch } else { @@ -267,7 +267,7 @@ impl State for WalletSettingsState { Some(daemon), ImportExportType::Descriptor(self.wallet.main_descriptor.clone()), ); - let launch = modal.launch(); + let launch = modal.launch(false); self.modal = Modal::ImportExport(modal); launch } else { diff --git a/liana-gui/src/app/state/transactions.rs b/liana-gui/src/app/state/transactions.rs index 03970daf..3aca960f 100644 --- a/liana-gui/src/app/state/transactions.rs +++ b/liana-gui/src/app/state/transactions.rs @@ -273,7 +273,7 @@ impl State for TransactionsPanel { ImportExportType::Transactions, )); if let TransactionsModal::Export(m) = &self.modal { - return m.launch(); + return m.launch(true); } } } diff --git a/liana-gui/src/app/view/export.rs b/liana-gui/src/app/view/export.rs index 62c63d6f..6df0f689 100644 --- a/liana-gui/src/app/view/export.rs +++ b/liana-gui/src/app/view/export.rs @@ -11,15 +11,17 @@ use liana_ui::{ widget::Element, }; -use crate::export::{Error, ImportExportMessage, ImportExportState}; +use crate::export::ImportExportState; +use crate::export::{Error, ImportExportMessage, ImportExportType}; /// Return the modal view for an export task pub fn export_modal<'a, Message: From + Clone + 'a>( state: &ImportExportState, error: Option<&'a Error>, - export_type: &str, + title: &str, + import_export_type: &ImportExportType, ) -> Element<'a, Message> { - let button = match state { + let cancel_close = match state { ImportExportState::Started | ImportExportState::Progress(_) => { Some(button::secondary(None, "Cancel").on_press(ImportExportMessage::UserStop.into())) } @@ -27,7 +29,9 @@ pub fn export_modal<'a, Message: From + Clone + 'a>( Some(button::secondary(None, "Close").on_press(ImportExportMessage::Close.into())) } _ => None, - }; + } + .map(Container::new); + let msg = if let Some(error) = error { format!("{:?}", error) } else { @@ -41,10 +45,53 @@ pub fn export_modal<'a, Message: From + Clone + 'a>( ImportExportState::Progress(p) => format!("Progress: {}%", p.round()), ImportExportState::TimedOut => "Export failed: timeout".into(), ImportExportState::Aborted => "Export canceled".into(), - ImportExportState::Ended => "Export successful!".into(), + ImportExportState::Ended => import_export_type.end_message().into(), ImportExportState::Closed => "".into(), } }; + let labels_btn = ( + "Labels conflict, what do you want to do?".to_string(), + Some(Container::new( + Row::new() + .push( + button::secondary(None, "Overwrite") + .on_press(ImportExportMessage::Overwrite.into()), + ) + .push(Space::with_width(30)) + .push( + button::secondary(None, "Ignore").on_press(ImportExportMessage::Ignore.into()), + ), + )), + ); + let aliases_btn = ( + "Aliases conflict, what do you want to do?".to_string(), + Some(Container::new( + Row::new() + .push( + button::secondary(None, "Overwrite") + .on_press(ImportExportMessage::Overwrite.into()), + ) + .push(Space::with_width(30)) + .push( + button::secondary(None, "Ignore").on_press(ImportExportMessage::Ignore.into()), + ), + )), + ); + let (msg, button) = match (error.is_none(), import_export_type) { + (true, ImportExportType::ImportBackup(labels, aliases)) => match (labels, aliases) { + (Some(_), _) => labels_btn, + + (_, Some(_)) => aliases_btn, + _ => (msg, cancel_close), + }, + _ => (msg, cancel_close), + }; + let button = button.map(|b| { + Container::new(b) + .align_x(Horizontal::Center) + .width(Length::Fill) + }); + let p = match state { ImportExportState::Init => 0.0, ImportExportState::ChoosePath | ImportExportState::Path(_) | ImportExportState::Started => { @@ -63,17 +110,13 @@ pub fn export_modal<'a, Message: From + Clone + 'a>( card::simple( Column::new() .spacing(10) - .push(Container::new(h4_bold(export_type)).width(Length::Fill)) + .push(Container::new(h4_bold(title)).width(Length::Fill)) .push(Space::with_height(Length::Fill)) .push(progress_bar_row) .push(Space::with_height(Length::Fill)) .push(Row::new().push(text(msg))) .push(Space::with_height(Length::Fill)) - .push_maybe(button.map(|b| { - Container::new(b) - .align_x(Horizontal::Right) - .width(Length::Fill) - })) + .push_maybe(button) .push(Space::with_height(5)), ) .width(Length::Fixed(500.0)) diff --git a/liana-gui/src/backup.rs b/liana-gui/src/backup.rs index ddea022e..a3ca2c48 100644 --- a/liana-gui/src/backup.rs +++ b/liana-gui/src/backup.rs @@ -72,6 +72,12 @@ pub enum Error { TxTimeMissing, } +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:?}") + } +} + impl From for Error { fn from(value: DaemonError) -> Self { Error::Daemon(value.to_string()) @@ -437,7 +443,7 @@ impl Account { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct Key { pub key: Fingerprint, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -450,7 +456,7 @@ pub struct Key { pub metadata: Value, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub enum KeyRole { /// Key to be used in normal spending condition Main, @@ -462,7 +468,7 @@ pub enum KeyRole { Cosigning, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub enum KeyType { /// Main user Internal, diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index 6e546b90..fb797416 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -5,7 +5,7 @@ use std::{ path::PathBuf, str::FromStr, sync::{ - mpsc::{channel, Receiver, Sender}, + mpsc::{channel, sync_channel, Receiver, Sender, SyncSender}, Arc, Mutex, }, time, @@ -16,7 +16,10 @@ use liana::{ descriptors::LianaDescriptor, miniscript::bitcoin::{Amount, Psbt, Txid}, }; -use lianad::bip329::{error::ExportError, Labels}; +use lianad::{ + bip329::{error::ExportError, Labels}, + commands::LabelItem, +}; use tokio::{ task::{JoinError, JoinHandle}, time::sleep, @@ -25,7 +28,11 @@ use tokio::{ use iced::futures::{SinkExt, Stream}; use crate::{ - app::view, + app::{ + settings::{KeySetting, Settings}, + view, + }, + backup::Backup, daemon::{ model::{HistoryTransaction, Labelled}, Daemon, DaemonBackend, DaemonError, @@ -61,7 +68,7 @@ macro_rules! send_progress { }; } -macro_rules! open_file { +macro_rules! open_file_write { ($path:ident, $sender:ident) => {{ let dir = match $path.parent() { Some(dir) => dir, @@ -86,6 +93,18 @@ macro_rules! open_file { }}; } +macro_rules! open_file_read { + ($path:ident, $sender:ident) => {{ + match File::open($path.as_path()) { + Ok(f) => f, + Err(e) => { + send_error!($sender, e.into()); + return; + } + } + }}; +} + #[derive(Debug, Clone)] pub enum ImportExportMessage { Open, @@ -94,6 +113,8 @@ pub enum ImportExportMessage { UserStop, Path(Option), Close, + Overwrite, + Ignore, } impl From for view::Message { @@ -129,19 +150,53 @@ pub enum Error { ParsePsbt, ParseDescriptor, Bip329Export(String), + BackupImport(String), } -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, Clone)] pub enum ImportExportType { Transactions, ExportPsbt(String), ExportBackup(String), + ImportBackup( + Option>, /*overwrite_labels*/ + Option>, /*overwrite_aliases*/ + ), Descriptor(LianaDescriptor), ExportLabels, ImportPsbt, ImportDescriptor, } +impl ImportExportType { + pub fn end_message(&self) -> &str { + match self { + ImportExportType::Transactions + | ImportExportType::ExportPsbt(_) + | ImportExportType::ExportBackup(_) + | ImportExportType::Descriptor(_) + | ImportExportType::ExportLabels => "Export successful!", + ImportExportType::ImportBackup(_, _) + | ImportExportType::ImportPsbt + | ImportExportType::ImportDescriptor => "Import successful", + } + } +} + +impl PartialEq for ImportExportType { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::ExportPsbt(l0), Self::ExportPsbt(r0)) => l0 == r0, + (Self::ExportBackup(l0), Self::ExportBackup(r0)) => l0 == r0, + (Self::ImportBackup(l0, l1), Self::ImportBackup(r0, r1)) => { + l0.is_some() == r0.is_some() && l1.is_some() == r1.is_some() + } + (Self::Descriptor(l0), Self::Descriptor(r0)) => l0 == r0, + _ => core::mem::discriminant(self) == core::mem::discriminant(other), + } + } +} + impl From for Error { fn from(value: JoinError) -> Self { Error::JoinError(format!("{:?}", value)) @@ -183,6 +238,8 @@ pub enum Progress { None, Psbt(Psbt), Descriptor(LianaDescriptor), + LabelsConflict(SyncSender), + KeyAliasesConflict(SyncSender), } pub struct Export { @@ -225,6 +282,7 @@ impl Export { ImportExportType::ImportPsbt => import_psbt(sender, path), ImportExportType::ImportDescriptor => import_descriptor(sender, path), ImportExportType::ExportBackup(str) => export_string(sender, path, str), + ImportExportType::ImportBackup(..) => import_backup(sender, path, daemon).await, }; } @@ -335,7 +393,7 @@ pub async fn export_transactions( return; } }; - let mut file = open_file!(path, sender); + let mut file = open_file_write!(path, sender); let header = "Date,Label,Value,Fee,Txid,Block\n".to_string(); if let Err(e) = file.write_all(header.as_bytes()) { @@ -488,7 +546,7 @@ pub async fn export_transactions( } pub fn export_descriptor(sender: Sender, path: PathBuf, descriptor: LianaDescriptor) { - let mut file = open_file!(path, sender); + let mut file = open_file_write!(path, sender); let descr_string = descriptor.to_string(); if let Err(e) = file.write_all(descr_string.as_bytes()) { @@ -500,7 +558,7 @@ pub fn export_descriptor(sender: Sender, path: PathBuf, descriptor: Li } pub fn export_string(sender: Sender, path: PathBuf, psbt: String) { - let mut file = open_file!(path, sender); + let mut file = open_file_write!(path, sender); if let Err(e) = file.write_all(psbt.as_bytes()) { send_error!(sender, e.into()); @@ -511,7 +569,7 @@ pub fn export_string(sender: Sender, path: PathBuf, psbt: String) { } pub fn import_psbt(sender: Sender, path: PathBuf) { - let mut file = open_file!(path, sender); + let mut file = open_file_read!(path, sender); let mut psbt_str = String::new(); if let Err(e) = file.read_to_string(&mut psbt_str) { @@ -532,7 +590,7 @@ pub fn import_psbt(sender: Sender, path: PathBuf) { } pub fn import_descriptor(sender: Sender, path: PathBuf) { - let mut file = open_file!(path, sender); + let mut file = open_file_read!(path, sender); let mut descr_str = String::new(); if let Err(e) = file.read_to_string(&mut descr_str) { @@ -552,6 +610,336 @@ pub fn import_descriptor(sender: Sender, path: PathBuf) { send_progress!(sender, Descriptor(descriptor)); } +/// Import a backup in an already existing wallet: +/// - Load backup from file +/// - check if networks matches +/// - check if descriptors matches +/// - check if labels can be imported w/o conflict, if conflic ask user to ACK +/// - check if aliases can be imported w/o conflict, if conflict ask user to ACK +/// - update receive and change indexes +/// - parse psbt from backup +/// - import PSBTs +/// - import labels if no conflict or user ACK +/// - update aliases if no conflict or user ACK +pub async fn import_backup( + sender: Sender, + path: PathBuf, + daemon: Option>, +) { + let daemon = match daemon { + Some(d) => d, + None => { + send_error!(sender, Error::DaemonMissing); + return; + } + }; + + // TODO: drop after support for restore to liana-connect + if matches!(daemon.backend(), DaemonBackend::RemoteBackend) { + send_error!( + sender, + Error::BackupImport("Restore to a Liana-connect backend is not yet supported!".into()) + ); + return; + } + + // Load backup from file + let mut file = open_file_read!(path, sender); + + let mut backup_str = String::new(); + if let Err(e) = file.read_to_string(&mut backup_str) { + send_error!(sender, e.into()); + return; + } + + let backup: Result = serde_json::from_str(&backup_str); + let backup = match backup { + Ok(psbt) => psbt, + Err(e) => { + send_error!(sender, Error::BackupImport(format!("{:?}", e))); + return; + } + }; + + // get backend info + let info = match daemon.get_info().await { + Ok(info) => info, + Err(e) => { + send_error!(sender, Error::Daemon(format!("{e:?}"))); + return; + } + }; + + // check if networks matches + let network = info.network; + if backup.network != network { + send_error!( + sender, + Error::BackupImport("The network of the backup don't match the wallet network!".into()) + ); + return; + } + + // check if descriptors matches + let descriptor = info.descriptors.main; + let account = match backup.accounts.len() { + 0 => { + send_error!( + sender, + Error::BackupImport("There is no account in the backup!".into()) + ); + return; + } + 1 => backup.accounts.first().expect("already checked"), + _ => { + send_error!( + sender, + Error::BackupImport( + "Liana is actually not supporting import of backup with several accounts!" + .into() + ) + ); + return; + } + }; + + let backup_descriptor = match LianaDescriptor::from_str(&account.descriptor) { + Ok(d) => d, + Err(_) => { + send_error!( + sender, + Error::BackupImport( + "The backup descriptor is not a valid Liana descriptor!".into() + ) + ); + return; + } + }; + + if backup_descriptor != descriptor { + send_error!( + sender, + Error::BackupImport("The backup descriptor do not match this wallet!".into()) + ); + return; + } + + // TODO: check if timestamp matches? + + // check if labels can be imported w/o conflict + let mut write_labels = true; + let backup_labels = if let Some(labels) = account.labels.clone() { + let db_labels = match daemon.get_labels_bip329(0, u32::MAX).await { + Ok(l) => l, + Err(_) => { + send_error!(sender, Error::BackupImport("Fail to dump DB labels".into())); + return; + } + }; + + let labels_map = db_labels.clone().into_map(); + let backup_labels_map = labels.clone().into_map(); + + // if there is a conflict, we ask user to ACK before overwrite + let (ack_sender, ack_receiver) = sync_channel(0); + let mut conflict = false; + for (k, l) in &backup_labels_map { + if let Some(lab) = labels_map.get(k) { + if lab != l { + send_progress!(sender, LabelsConflict(ack_sender)); + conflict = true; + break; + } + } + } + if conflict { + write_labels = match ack_receiver.recv() { + Ok(b) => b, + Err(_) => { + send_error!( + sender, + Error::BackupImport("Fail to receive labels ACK".into()) + ); + return; + } + } + } + + labels.into_vec() + } else { + Vec::new() + }; + + let datadir = match daemon.config() { + Some(c) => match &c.data_dir { + Some(dd) => dd, + None => { + send_error!( + sender, + Error::BackupImport("Fail to get Daemon config".into()) + ); + return; + } + }, + None => { + send_error!( + sender, + Error::BackupImport("Fail to get Daemon config".into()) + ); + return; + } + }; + + // check if key aliases can be imported w/o conflict + let mut write_aliases = true; + let settings = if !account.keys.is_empty() { + let settings = match Settings::from_file(datadir.to_path_buf(), network) { + Ok(s) => s, + Err(_) => { + send_error!( + sender, + Error::BackupImport("Fail to get App Settings".into()) + ); + return; + } + }; + + let settings_aliases: HashMap<_, _> = match settings.wallets.len() { + 1 => settings + .wallets + .first() + .expect("already checked") + .keys + .clone() + .into_iter() + .map(|s| (s.master_fingerprint, s)) + .collect(), + _ => { + send_error!( + sender, + Error::BackupImport("Settings.wallets.len() is not 1".into()) + ); + return; + } + }; + + let (ack_sender, ack_receiver) = sync_channel(0); + let mut conflict = false; + for (fg, key) in &account.keys { + if let Some(k) = settings_aliases.get(fg) { + let ks = k.to_backup(); + if ks != *key { + send_progress!(sender, KeyAliasesConflict(ack_sender)); + conflict = true; + break; + } + } + } + if conflict { + // wait for the user ACK/NACK + write_aliases = match ack_receiver.recv() { + Ok(a) => a, + Err(_) => { + send_error!( + sender, + Error::BackupImport("Fail to receive aliases ACK".into()) + ); + return; + } + }; + } + + Some((settings, settings_aliases)) + } else { + None + }; + + // update receive & change index + let db_receive = info.receive_index; + let i = account.receive_index.unwrap_or(0); + let receive = if db_receive < i { Some(i) } else { None }; + + let db_change = info.change_index; + let i = account.change_index.unwrap_or(0); + let change = if db_change < i { Some(i) } else { None }; + + if daemon.update_deriv_indexes(receive, change).await.is_err() { + send_error!( + sender, + Error::BackupImport("Fail to update derivation indexes".into()) + ); + return; + } + + // parse PSBTs + let mut psbts = Vec::new(); + for psbt_str in &account.psbts { + match Psbt::from_str(psbt_str) { + Ok(p) => { + psbts.push(p); + } + Err(_) => { + send_error!(sender, Error::BackupImport("Fail to parse PSBT".into())); + return; + } + } + } + + // import PSBTs + for psbt in psbts { + if daemon.update_spend_tx(&psbt).await.is_err() { + send_error!(sender, Error::BackupImport("Fail to store PSBT".into())); + return; + } + } + + // import labels if no conflict or user ACK + if write_labels { + let labels: HashMap> = backup_labels + .into_iter() + .filter_map(|l| { + if let Some((item, label)) = LabelItem::from_bip329(&l, network) { + Some((item, Some(label))) + } else { + None + } + }) + .collect(); + if daemon.update_labels(&labels).await.is_err() { + send_error!(sender, Error::BackupImport("Fail to import labels".into())); + return; + } + } + + // update aliases if no conflict or user ACK + if let (true, Some((mut settings, mut settings_aliases))) = (write_aliases, settings) { + for (k, v) in &account.keys { + if let Some(ks) = KeySetting::from_backup( + v.alias.clone().unwrap_or("".into()), + *k, + v.role.clone(), + v.key_type.clone(), + v.metadata.clone(), + ) { + settings_aliases.insert(*k, ks); + } + } + + settings.wallets.get_mut(0).expect("already checked").keys = + settings_aliases.into_values().collect(); + if settings.to_file(datadir.to_path_buf(), network).is_err() { + send_error!( + sender, + Error::BackupImport("Fail to import keys aliases".into()) + ); + return; + } + } + + send_progress!(sender, Progress(100.0)); + send_progress!(sender, Ended); +} + pub async fn export_labels( sender: Sender, daemon: Option>, @@ -590,7 +978,7 @@ pub async fn export_labels( return; } }; - let mut file = open_file!(path, sender); + let mut file = open_file_write!(path, sender); if let Err(e) = file.write_all(json.as_bytes()) { send_error!(sender, e.into()); @@ -600,11 +988,20 @@ pub async fn export_labels( send_progress!(sender, Ended); } -pub async fn get_path(filename: String) -> Option { - rfd::AsyncFileDialog::new() - .set_title("Choose a location to export...") - .set_file_name(filename) - .save_file() - .await - .map(|fh| fh.path().to_path_buf()) +pub async fn get_path(filename: String, write: bool) -> Option { + if write { + rfd::AsyncFileDialog::new() + .set_title("Choose a location to export...") + .set_file_name(filename) + .save_file() + .await + .map(|fh| fh.path().to_path_buf()) + } else { + rfd::AsyncFileDialog::new() + .set_title("Choose a file to import...") + .set_file_name(filename) + .pick_file() + .await + .map(|fh| fh.path().to_path_buf()) + } } diff --git a/liana-gui/src/installer/step/descriptor/mod.rs b/liana-gui/src/installer/step/descriptor/mod.rs index 0bf6d31e..c1dcd91e 100644 --- a/liana-gui/src/installer/step/descriptor/mod.rs +++ b/liana-gui/src/installer/step/descriptor/mod.rs @@ -345,7 +345,7 @@ impl Step for BackupDescriptor { } }; let modal = ExportModal::new(None, ImportExportType::ExportBackup(str)); - let launch = modal.launch(); + let launch = modal.launch(true); self.modal = Some(modal); return launch; } diff --git a/lianad/Cargo.toml b/lianad/Cargo.toml index 6d0d3727..d39b37a0 100644 --- a/lianad/Cargo.toml +++ b/lianad/Cargo.toml @@ -55,4 +55,6 @@ rusqlite = { version = "0.30", features = ["bundled", "unlock_notify"] } # To talk to bitcoind jsonrpc = { version = "0.17", features = ["minreq_http"], default-features = false } -bip329 = {version = "0.1.1", default-features = false } + +# import/export labels +bip329 = "0.3.0" diff --git a/lianad/src/database/mod.rs b/lianad/src/database/mod.rs index 4265364a..cd1bd22b 100644 --- a/lianad/src/database/mod.rs +++ b/lianad/src/database/mod.rs @@ -576,7 +576,7 @@ impl LabelItem { match label { bip329::Label::Transaction(tx_record) => { if let (Some(txid), Some(label)) = ( - Txid::from_str(&tx_record.ref_).ok(), + Txid::from_str(&tx_record.ref_.to_string()).ok(), tx_record.label.clone(), ) { Some((Self::Txid(txid), label)) @@ -586,7 +586,8 @@ impl LabelItem { } bip329::Label::Address(address_record) => { if let (Some(addr), Some(label)) = ( - Address::from_str(&address_record.ref_).ok(), + Address::from_str(&address_record.ref_.clone().assume_checked().to_string()) + .ok(), address_record.label.clone(), ) { if addr.is_valid_for_network(network) { @@ -600,7 +601,7 @@ impl LabelItem { } bip329::Label::Output(output_record) => { if let (Some(outpoint), Some(label)) = ( - OutPoint::from_str(&output_record.ref_).ok(), + OutPoint::from_str(&output_record.ref_.to_string()).ok(), output_record.label.clone(), ) { Some((Self::OutPoint(outpoint), label)) diff --git a/lianad/src/database/sqlite/schema.rs b/lianad/src/database/sqlite/schema.rs index 7b71990b..0eafed73 100644 --- a/lianad/src/database/sqlite/schema.rs +++ b/lianad/src/database/sqlite/schema.rs @@ -3,7 +3,14 @@ use liana::descriptors::LianaDescriptor; use std::{convert::TryFrom, str::FromStr}; -use miniscript::bitcoin::{self, address, bip32, consensus::encode, psbt::Psbt}; +use miniscript::bitcoin::{ + self, + address::{self, NetworkUnchecked}, + bip32, + consensus::encode, + psbt::Psbt, + Address, OutPoint, +}; // Due to limitations of Sqlite's ALTER TABLE command and in order not to recreate // tables during migration: @@ -376,14 +383,19 @@ impl From for Label { Some(value.value) }; match value.item_kind { - DbLabelledKind::Address => Label::Address(bip329::AddressRecord { ref_, label }), - DbLabelledKind::OutPoint => Label::Output(bip329::OutputRecord { - ref_, + DbLabelledKind::Address => Label::Address(bip329::AddressRecord { + ref_: Address::::from_str(&ref_) + .expect("db contains valid adresses"), label, - spendable: None, + }), + DbLabelledKind::OutPoint => Label::Output(bip329::OutputRecord { + ref_: OutPoint::from_str(&ref_).expect(" db contais valid outpoints"), + label, + spendable: true, }), DbLabelledKind::Txid => Label::Transaction(bip329::TransactionRecord { - ref_, + ref_: bitcoin::consensus::encode::deserialize_hex(&ref_) + .expect("db contains valid txid"), label, // FIXME: "Optional key origin information referencing the wallet associated with the label" origin: None, From 2fe297879b022d5da9e562816774d70075e40536 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Wed, 12 Mar 2025 03:35:15 +0100 Subject: [PATCH 27/44] gui: integrate restore backup to settings --- liana-gui/src/app/error.rs | 3 ++ liana-gui/src/app/state/export.rs | 6 ++++ liana-gui/src/app/state/settings/mod.rs | 40 ++++++++++++++-------- liana-gui/src/app/state/settings/wallet.rs | 36 ++++++++++++------- liana-gui/src/app/view/warning.rs | 1 + liana-gui/src/export.rs | 6 +++- 6 files changed, 63 insertions(+), 29 deletions(-) diff --git a/liana-gui/src/app/error.rs b/liana-gui/src/app/error.rs index 4050ed88..4c5cee82 100644 --- a/liana-gui/src/app/error.rs +++ b/liana-gui/src/app/error.rs @@ -7,6 +7,7 @@ use lianad::config::ConfigError; use crate::{ app::{settings::SettingsError, wallet::WalletError}, daemon::DaemonError, + export, }; #[derive(Debug)] @@ -18,6 +19,7 @@ pub enum Error { HardwareWallet(async_hwi::Error), Desc(LianaDescError), Spend(SpendCreationError), + ImportExport(export::Error), } impl std::fmt::Display for Error { @@ -58,6 +60,7 @@ impl std::fmt::Display for Error { Self::Unexpected(e) => write!(f, "Unexpected error: {}", e), Self::HardwareWallet(e) => write!(f, "error: {}\nPlease check if the device is still connected and unlocked with the correct firmware open for the current network and no other application is accessing the device.", e), Self::Desc(e) => write!(f, "Liana descriptor error: {}", e), + Self::ImportExport(e) => write!(f, "{e:?}"), } } } diff --git a/liana-gui/src/app/state/export.rs b/liana-gui/src/app/state/export.rs index 1e413705..7de48c93 100644 --- a/liana-gui/src/app/state/export.rs +++ b/liana-gui/src/app/state/export.rs @@ -127,6 +127,11 @@ impl ExportModal { } // TODO: forward Descriptor } + Progress::UpdateAliases(map) => { + return Task::perform(async {}, move |_| { + ImportExportMessage::UpdateAliases(map.clone()).into() + }); + } }, ImportExportMessage::TimedOut => { self.stop(ImportExportState::TimedOut); @@ -173,6 +178,7 @@ impl ExportModal { } } } + ImportExportMessage::UpdateAliases(_) => { /* unexpected */ } } Task::none() } diff --git a/liana-gui/src/app/state/settings/mod.rs b/liana-gui/src/app/state/settings/mod.rs index d1bc6a0a..4e73a94c 100644 --- a/liana-gui/src/app/state/settings/mod.rs +++ b/liana-gui/src/app/state/settings/mod.rs @@ -10,7 +10,7 @@ use iced::Task; use liana_ui::{component::form, widget::Element}; use bitcoind::BitcoindSettingsState; -use wallet::WalletSettingsState; +use wallet::{app_backup, WalletSettingsState}; use crate::{ app::{ @@ -22,9 +22,8 @@ use crate::{ wallet::Wallet, Config, }, - backup::Backup, daemon::{Daemon, DaemonBackend}, - export::{ImportExportMessage, ImportExportType}, + export::{self, ImportExportMessage, ImportExportType}, }; use super::export::ExportModal; @@ -224,6 +223,14 @@ impl State for ImportExportSettingsState { self.modal = None; } Message::View(view::Message::ImportExport(m)) => { + if let ImportExportMessage::UpdateAliases(aliases) = m { + let mut wallet = (*self.wallet).clone(); + wallet.keys_aliases = aliases; + let wallet = Arc::new(wallet); + return Task::perform(async {}, move |_| { + Message::WalletUpdated(Ok(wallet.clone())) + }); + } if let Some(modal) = self.modal.as_mut() { return modal.update(m); }; @@ -262,29 +269,32 @@ impl State for ImportExportSettingsState { let wallet = self.wallet.clone(); let daemon = daemon.clone(); return Task::perform( - async move { - let backup = - Backup::from_app(datadir, network, config, wallet, daemon).await; - let backup = backup.unwrap(); - serde_json::to_string_pretty(&backup).unwrap() - // TODO: do not unwrap, return an error message instead - }, + async move { app_backup(datadir, network, config, wallet, daemon).await }, |s| { Message::View(view::Message::Settings( - view::SettingsMessage::ExportBackup(Ok(s)), + view::SettingsMessage::ExportBackup(s), )) }, ); } } - Message::View(view::Message::Settings(view::SettingsMessage::ExportBackup(Ok( - backup, - )))) => { + Message::View(view::Message::Settings(view::SettingsMessage::ExportBackup(backup))) => { + let backup = match backup { + Ok(b) => b, + Err(e) => { + self.warning = Some(Error::ImportExport(export::Error::Backup(e))); + return Task::none(); + } + }; let modal = ExportModal::new(Some(daemon), ImportExportType::ExportBackup(backup)); launch!(self, modal, true); } Message::View(view::Message::Settings(view::SettingsMessage::ImportWallet)) => { - // TODO: + if self.modal.is_none() { + let modal = + ExportModal::new(Some(daemon), ImportExportType::ImportBackup(None, None)); + launch!(self, modal, false); + } } _ => {} } diff --git a/liana-gui/src/app/state/settings/wallet.rs b/liana-gui/src/app/state/settings/wallet.rs index dd06a9c5..8b52f650 100644 --- a/liana-gui/src/app/state/settings/wallet.rs +++ b/liana-gui/src/app/state/settings/wallet.rs @@ -26,9 +26,9 @@ use crate::{ wallet::Wallet, Config, }, - backup::Backup, + backup::{self, Backup}, daemon::{Daemon, DaemonBackend}, - export::{ImportExportMessage, ImportExportType}, + export::{self, ImportExportMessage, ImportExportType}, hw::{HardwareWallet, HardwareWalletConfig, HardwareWallets}, }; @@ -231,16 +231,10 @@ impl State for WalletSettingsState { let wallet = self.wallet.clone(); let daemon = daemon.clone(); Task::perform( - async move { - let backup = - Backup::from_app(datadir, network, config, wallet, daemon).await; - let backup = backup.unwrap(); - serde_json::to_string_pretty(&backup).unwrap() - // TODO: do not unwrap, return an error message instead - }, + async move { app_backup(datadir, network, config, wallet, daemon).await }, |s| { Message::View(view::Message::Settings( - view::SettingsMessage::ExportBackup(Ok(s)), + view::SettingsMessage::ExportBackup(s), )) }, ) @@ -248,10 +242,15 @@ impl State for WalletSettingsState { Task::none() } } - Message::View(view::Message::Settings(view::SettingsMessage::ExportBackup(Ok( - backup, - )))) => { + Message::View(view::Message::Settings(view::SettingsMessage::ExportBackup(backup))) => { if self.modal.is_none() { + let backup = match backup { + Ok(b) => b, + Err(e) => { + self.warning = Some(Error::ImportExport(export::Error::Backup(e))); + return Task::none(); + } + }; let modal = ExportModal::new(Some(daemon), ImportExportType::ExportBackup(backup)); let launch = modal.launch(true); @@ -512,3 +511,14 @@ async fn update_keys_aliases( Ok(Arc::new(wallet)) } + +pub async fn app_backup( + datadir: PathBuf, + network: Network, + config: Arc, + wallet: Arc, + daemon: Arc, +) -> Result { + let backup = Backup::from_app(datadir, network, config, wallet, daemon).await?; + serde_json::to_string_pretty(&backup).map_err(|_| backup::Error::Json) +} diff --git a/liana-gui/src/app/view/warning.rs b/liana-gui/src/app/view/warning.rs index cdc18bd8..4af3fabe 100644 --- a/liana-gui/src/app/view/warning.rs +++ b/liana-gui/src/app/view/warning.rs @@ -49,6 +49,7 @@ impl From<&Error> for WarningMessage { Error::HardwareWallet(_) => WarningMessage("Hardware wallet error".to_string()), Error::Desc(e) => WarningMessage(format!("Descriptor analysis error: '{}'.", e)), Error::Spend(e) => WarningMessage(format!("Spend creation error: '{}'.", e)), + Error::ImportExport(e) => WarningMessage(format!("{e:?}")), } } } diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index fb797416..7bdf7e8d 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -11,6 +11,7 @@ use std::{ time, }; +use async_hwi::bitbox::api::btc::Fingerprint; use chrono::{DateTime, Duration, Utc}; use liana::{ descriptors::LianaDescriptor, @@ -32,7 +33,7 @@ use crate::{ settings::{KeySetting, Settings}, view, }, - backup::Backup, + backup::{self, Backup}, daemon::{ model::{HistoryTransaction, Labelled}, Daemon, DaemonBackend, DaemonError, @@ -115,6 +116,7 @@ pub enum ImportExportMessage { Close, Overwrite, Ignore, + UpdateAliases(HashMap), } impl From for view::Message { @@ -151,6 +153,7 @@ pub enum Error { ParseDescriptor, Bip329Export(String), BackupImport(String), + Backup(backup::Error), } #[derive(Debug, Clone)] @@ -240,6 +243,7 @@ pub enum Progress { Descriptor(LianaDescriptor), LabelsConflict(SyncSender), KeyAliasesConflict(SyncSender), + UpdateAliases(HashMap), } pub struct Export { From 29b1673460fd2160e2e768c30f5e7f151f065b92 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Thu, 13 Mar 2025 11:40:35 +0100 Subject: [PATCH 28/44] fix user ACK + call import in settings/wallet --- liana-gui/src/app/state/settings/wallet.rs | 6 ++---- liana-gui/src/app/view/export.rs | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/liana-gui/src/app/state/settings/wallet.rs b/liana-gui/src/app/state/settings/wallet.rs index 8b52f650..6db4930d 100644 --- a/liana-gui/src/app/state/settings/wallet.rs +++ b/liana-gui/src/app/state/settings/wallet.rs @@ -262,10 +262,8 @@ impl State for WalletSettingsState { } Message::View(view::Message::Settings(view::SettingsMessage::ImportWallet)) => { if self.modal.is_none() { - let modal = ExportModal::new( - Some(daemon), - ImportExportType::Descriptor(self.wallet.main_descriptor.clone()), - ); + let modal = + ExportModal::new(Some(daemon), ImportExportType::ImportBackup(None, None)); let launch = modal.launch(false); self.modal = Modal::ImportExport(modal); launch diff --git a/liana-gui/src/app/view/export.rs b/liana-gui/src/app/view/export.rs index 6df0f689..16a57490 100644 --- a/liana-gui/src/app/view/export.rs +++ b/liana-gui/src/app/view/export.rs @@ -77,8 +77,8 @@ pub fn export_modal<'a, Message: From + Clone + 'a>( ), )), ); - let (msg, button) = match (error.is_none(), import_export_type) { - (true, ImportExportType::ImportBackup(labels, aliases)) => match (labels, aliases) { + let (msg, button) = match import_export_type { + ImportExportType::ImportBackup(labels, aliases) => match (labels, aliases) { (Some(_), _) => labels_btn, (_, Some(_)) => aliases_btn, From e19644ae8cf0acf7bd21eec2a609f76cb6810293 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Wed, 12 Mar 2025 03:43:57 +0100 Subject: [PATCH 29/44] gui: display export error --- liana-gui/src/app/error.rs | 2 +- liana-gui/src/app/view/warning.rs | 2 +- liana-gui/src/backup.rs | 9 ++++++++- liana-gui/src/export.rs | 22 ++++++++++++++++++++++ 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/liana-gui/src/app/error.rs b/liana-gui/src/app/error.rs index 4c5cee82..dad8c47c 100644 --- a/liana-gui/src/app/error.rs +++ b/liana-gui/src/app/error.rs @@ -60,7 +60,7 @@ impl std::fmt::Display for Error { Self::Unexpected(e) => write!(f, "Unexpected error: {}", e), Self::HardwareWallet(e) => write!(f, "error: {}\nPlease check if the device is still connected and unlocked with the correct firmware open for the current network and no other application is accessing the device.", e), Self::Desc(e) => write!(f, "Liana descriptor error: {}", e), - Self::ImportExport(e) => write!(f, "{e:?}"), + Self::ImportExport(e) => write!(f, "{e}"), } } } diff --git a/liana-gui/src/app/view/warning.rs b/liana-gui/src/app/view/warning.rs index 4af3fabe..011ed430 100644 --- a/liana-gui/src/app/view/warning.rs +++ b/liana-gui/src/app/view/warning.rs @@ -49,7 +49,7 @@ impl From<&Error> for WarningMessage { Error::HardwareWallet(_) => WarningMessage("Hardware wallet error".to_string()), Error::Desc(e) => WarningMessage(format!("Descriptor analysis error: '{}'.", e)), Error::Spend(e) => WarningMessage(format!("Spend creation error: '{}'.", e)), - Error::ImportExport(e) => WarningMessage(format!("{e:?}")), + Error::ImportExport(e) => WarningMessage(format!("{e}")), } } } diff --git a/liana-gui/src/backup.rs b/liana-gui/src/backup.rs index a3ca2c48..c11150cd 100644 --- a/liana-gui/src/backup.rs +++ b/liana-gui/src/backup.rs @@ -74,7 +74,14 @@ pub enum Error { impl Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{self:?}") + match self { + Error::DescriptorMissing => write!(f, "Backup: descriptor missing"), + Error::NotSingleWallet => write!(f, "Backup: Zero or several wallets"), + Error::Json => write!(f, "Backup: json error"), + Error::SettingsFromFile => write!(f, "Backup: fail to parse setting from file"), + Error::Daemon(e) => write!(f, "Backup daemon error: {e}"), + Error::TxTimeMissing => write!(f, "Backup: transaction block height missing"), + } } } diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index 7bdf7e8d..77026043 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -1,5 +1,6 @@ use std::{ collections::HashMap, + fmt::Display, fs::{self, File}, io::{Read, Write}, path::PathBuf, @@ -156,6 +157,27 @@ pub enum Error { Backup(backup::Error), } +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Io(e) => write!(f, "ImportExport Io Error: {e}"), + Error::HandleLost => write!(f, "ImportExport: subprocess handle lost"), + Error::UnexpectedEnd => write!(f, "ImportExport: unexpected end of the process"), + Error::JoinError(e) => write!(f, "ImportExport fail to handle.join(): {e} "), + Error::ChannelLost => write!(f, "ImportExport: the channel have been closed"), + Error::NoParentDir => write!(f, "ImportExport: there is no parent dir"), + Error::Daemon(e) => write!(f, "ImportExport daemon error: {e}"), + Error::TxTimeMissing => write!(f, "ImportExport: transaction block height missing"), + Error::DaemonMissing => write!(f, "ImportExport: the daemon is missing"), + Error::ParsePsbt => write!(f, "ImportExport: fail to parse PSBT"), + Error::ParseDescriptor => write!(f, "ImportExport: fail to parse descriptor"), + Error::Bip329Export(e) => write!(f, "Bip329Export: {e}"), + Error::BackupImport(e) => write!(f, "BackupImport: {e}"), + Error::Backup(e) => write!(f, "Backup: {e}"), + } + } +} + #[derive(Debug, Clone)] pub enum ImportExportType { Transactions, From 33e39316c7c265ff64c55133fbf6e7ac908e660a Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Tue, 11 Mar 2025 08:33:20 +0100 Subject: [PATCH 30/44] import: implement import_backup_at_launch() --- liana-gui/src/app/error.rs | 4 +- liana-gui/src/app/state/settings/mod.rs | 13 +- liana-gui/src/app/view/warning.rs | 1 + liana-gui/src/export.rs | 174 +++++++++++++++++++++++- 4 files changed, 185 insertions(+), 7 deletions(-) diff --git a/liana-gui/src/app/error.rs b/liana-gui/src/app/error.rs index dad8c47c..ce49658e 100644 --- a/liana-gui/src/app/error.rs +++ b/liana-gui/src/app/error.rs @@ -7,7 +7,7 @@ use lianad::config::ConfigError; use crate::{ app::{settings::SettingsError, wallet::WalletError}, daemon::DaemonError, - export, + export::{self, RestoreBackupError}, }; #[derive(Debug)] @@ -20,6 +20,7 @@ pub enum Error { Desc(LianaDescError), Spend(SpendCreationError), ImportExport(export::Error), + RestoreBackup(RestoreBackupError), } impl std::fmt::Display for Error { @@ -61,6 +62,7 @@ impl std::fmt::Display for Error { Self::HardwareWallet(e) => write!(f, "error: {}\nPlease check if the device is still connected and unlocked with the correct firmware open for the current network and no other application is accessing the device.", e), Self::Desc(e) => write!(f, "Liana descriptor error: {}", e), Self::ImportExport(e) => write!(f, "{e}"), + Self::RestoreBackup(e) => write!(f, "{e}"), } } } diff --git a/liana-gui/src/app/state/settings/mod.rs b/liana-gui/src/app/state/settings/mod.rs index 4e73a94c..774de791 100644 --- a/liana-gui/src/app/state/settings/mod.rs +++ b/liana-gui/src/app/state/settings/mod.rs @@ -1,9 +1,9 @@ mod bitcoind; mod wallet; -use std::convert::From; use std::path::PathBuf; use std::sync::Arc; +use std::{collections::HashMap, convert::From}; use iced::Task; @@ -225,7 +225,16 @@ impl State for ImportExportSettingsState { Message::View(view::Message::ImportExport(m)) => { if let ImportExportMessage::UpdateAliases(aliases) = m { let mut wallet = (*self.wallet).clone(); - wallet.keys_aliases = aliases; + let mut ka = HashMap::new(); + let mut pk = HashMap::new(); + aliases.iter().for_each(|(k, ks)| { + ka.insert(*k, ks.name()); + if let Some(p_key) = &ks.provider_key { + pk.insert(*k, p_key.clone()); + } + }); + wallet.keys_aliases = ka; + wallet.provider_keys = pk; let wallet = Arc::new(wallet); return Task::perform(async {}, move |_| { Message::WalletUpdated(Ok(wallet.clone())) diff --git a/liana-gui/src/app/view/warning.rs b/liana-gui/src/app/view/warning.rs index 011ed430..1f371481 100644 --- a/liana-gui/src/app/view/warning.rs +++ b/liana-gui/src/app/view/warning.rs @@ -50,6 +50,7 @@ impl From<&Error> for WarningMessage { Error::Desc(e) => WarningMessage(format!("Descriptor analysis error: '{}'.", e)), Error::Spend(e) => WarningMessage(format!("Spend creation error: '{}'.", e)), Error::ImportExport(e) => WarningMessage(format!("{e}")), + Error::RestoreBackup(e) => WarningMessage(format!("Fail to restore backup: {e}")), } } } diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index 77026043..7a38f78f 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -31,8 +31,11 @@ use iced::futures::{SinkExt, Stream}; use crate::{ app::{ - settings::{KeySetting, Settings}, + cache::Cache, + settings::{self, KeySetting, Settings}, view, + wallet::Wallet, + Config, }, backup::{self, Backup}, daemon::{ @@ -40,6 +43,7 @@ use crate::{ Daemon, DaemonBackend, DaemonError, }, lianalite::client::backend::api::DEFAULT_LIMIT, + node::bitcoind::Bitcoind, }; const DUMP_LABELS_LIMIT: u32 = 100; @@ -117,7 +121,7 @@ pub enum ImportExportMessage { Close, Overwrite, Ignore, - UpdateAliases(HashMap), + UpdateAliases(HashMap), } impl From for view::Message { @@ -265,7 +269,7 @@ pub enum Progress { Descriptor(LianaDescriptor), LabelsConflict(SyncSender), KeyAliasesConflict(SyncSender), - UpdateAliases(HashMap), + UpdateAliases(HashMap), } pub struct Export { @@ -952,13 +956,16 @@ pub async fn import_backup( } settings.wallets.get_mut(0).expect("already checked").keys = - settings_aliases.into_values().collect(); + settings_aliases.clone().into_values().collect(); if settings.to_file(datadir.to_path_buf(), network).is_err() { send_error!( sender, Error::BackupImport("Fail to import keys aliases".into()) ); return; + } else { + // Update wallet state + send_progress!(sender, UpdateAliases(settings_aliases)); } } @@ -966,6 +973,165 @@ pub async fn import_backup( send_progress!(sender, Ended); } +#[derive(Debug)] +pub enum RestoreBackupError { + Daemon(DaemonError), + Network, + InvalidDescriptor, + WrongDescriptor, + NoAccount, + SeveralAccounts, + LianaConnectNotSupported, + GetLabels, + LabelsNotEmpty, + NotImplemented, + InvalidPsbt, +} + +impl Display for RestoreBackupError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RestoreBackupError::Daemon(e) => write!(f, "Daemon error during restore process: {e}"), + RestoreBackupError::Network => write!(f, "Backup & wallet network don't matches"), + RestoreBackupError::InvalidDescriptor => write!(f, "The backup descriptor is invalid"), + RestoreBackupError::WrongDescriptor => { + write!(f, "Backup & wallet descriptor don't matches") + } + RestoreBackupError::NoAccount => write!(f, "There is no account in the backup"), + RestoreBackupError::SeveralAccounts => { + write!(f, "There is several accounts in the backup") + } + RestoreBackupError::LianaConnectNotSupported => { + write!(f, "Restore a backup to Liana-connect is not yet supported") + } + RestoreBackupError::GetLabels => write!(f, "Fails to get labels during backup restore"), + RestoreBackupError::LabelsNotEmpty => write!( + f, + "Cannot load labels: there is already labels into the database" + ), + RestoreBackupError::NotImplemented => write!(f, "Not implemented"), + RestoreBackupError::InvalidPsbt => write!(f, "Psbt is invalid"), + } + } +} + +impl From for RestoreBackupError { + fn from(value: DaemonError) -> Self { + Self::Daemon(value) + } +} + +#[allow(unused)] +/// Import backup data if wallet created from a backup +/// - check if networks matches +/// - check if descriptors matches +/// - check if labels are empty +/// - update receive and change indexes +/// - parse psbt from backup +/// - import PSBTs +/// - import labels +pub async fn import_backup_at_launch( + cache: Cache, + wallet: Arc, + config: Config, + daemon: Arc, + datadir: PathBuf, + internal_bitcoind: Option, + backup: Backup, +) -> Result< + ( + Cache, + Arc, + Config, + Arc, + PathBuf, + Option, + ), + RestoreBackupError, +> { + // TODO: drop after support for restore to liana-connect + if matches!(daemon.backend(), DaemonBackend::RemoteBackend) { + return Err(RestoreBackupError::LianaConnectNotSupported); + } + + // get backend info + let info = daemon.get_info().await?; + + // check if networks matches + let network = info.network; + if backup.network != network { + return Err(RestoreBackupError::Network); + } + + // check if descriptors matches + let descriptor = info.descriptors.main; + let account = match backup.accounts.len() { + 0 => return Err(RestoreBackupError::NoAccount), + 1 => backup.accounts.first().expect("already checked"), + _ => return Err(RestoreBackupError::SeveralAccounts), + }; + + let backup_descriptor = LianaDescriptor::from_str(&account.descriptor) + .map_err(|_| RestoreBackupError::InvalidDescriptor)?; + + if backup_descriptor != descriptor { + return Err(RestoreBackupError::WrongDescriptor); + } + + // check there is no labels in DB + if account.labels.is_some() + && !daemon + .get_labels_bip329(0, u32::MAX) + .await + .map_err(|_| RestoreBackupError::GetLabels)? + .to_vec() + .is_empty() + { + return Err(RestoreBackupError::LabelsNotEmpty); + } + + // parse PSBTs + let mut psbts = Vec::new(); + for psbt_str in &account.psbts { + psbts.push(Psbt::from_str(psbt_str).map_err(|_| RestoreBackupError::InvalidPsbt)?); + } + + // update receive & change index + let db_receive = info.receive_index; + let i = account.receive_index.unwrap_or(0); + let receive = if db_receive < i { Some(i) } else { None }; + + let db_change = info.change_index; + let i = account.change_index.unwrap_or(0); + let change = if db_change < i { Some(i) } else { None }; + + daemon.update_deriv_indexes(receive, change).await?; + + // import labels + if let Some(labels) = account.labels.clone().map(|l| l.into_vec()) { + let labels: HashMap> = labels + .into_iter() + .filter_map(|l| { + if let Some((item, label)) = LabelItem::from_bip329(&l, network) { + Some((item, Some(label))) + } else { + None + } + }) + .collect(); + daemon.update_labels(&labels).await?; + } + + // import PSBTs + for psbt in psbts { + if let Err(e) = daemon.update_spend_tx(&psbt).await { + tracing::error!("Fail to restore PSBT: {e}") + } + } + + Ok((cache, wallet, config, daemon, datadir, internal_bitcoind)) +} + pub async fn export_labels( sender: Sender, daemon: Option>, From 03476de35abd779de95595f538b081d2c945285f Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Thu, 13 Mar 2025 06:29:27 +0100 Subject: [PATCH 31/44] gui: call import_backup_at_launch --- liana-gui/src/backup.rs | 10 +++--- liana-gui/src/export.rs | 4 +-- liana-gui/src/installer/context.rs | 3 ++ liana-gui/src/installer/mod.rs | 2 +- liana-gui/src/loader.rs | 30 ++++++++++++++-- liana-gui/src/main.rs | 56 +++++++++++++++++++++++------- 6 files changed, 82 insertions(+), 23 deletions(-) diff --git a/liana-gui/src/backup.rs b/liana-gui/src/backup.rs index c11150cd..d66d9c9d 100644 --- a/liana-gui/src/backup.rs +++ b/liana-gui/src/backup.rs @@ -43,7 +43,7 @@ fn now() -> u64 { .as_secs() } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct Backup { #[serde(default, skip_serializing_if = "Option::is_none")] pub name: Option, @@ -371,7 +371,7 @@ async fn get_transactions( Ok(vec) } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct Account { #[serde(default, skip_serializing_if = "Option::is_none")] pub name: Option, @@ -450,7 +450,7 @@ impl Account { } } -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct Key { pub key: Fingerprint, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -463,7 +463,7 @@ pub struct Key { pub metadata: Value, } -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] pub enum KeyRole { /// Key to be used in normal spending condition Main, @@ -475,7 +475,7 @@ pub enum KeyRole { Cosigning, } -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] pub enum KeyType { /// Main user Internal, diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index 7a38f78f..c50b5bdd 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -947,8 +947,8 @@ pub async fn import_backup( if let Some(ks) = KeySetting::from_backup( v.alias.clone().unwrap_or("".into()), *k, - v.role.clone(), - v.key_type.clone(), + v.role, + v.key_type, v.metadata.clone(), ) { settings_aliases.insert(*k, ks); diff --git a/liana-gui/src/installer/context.rs b/liana-gui/src/installer/context.rs index c1071ff1..025349bc 100644 --- a/liana-gui/src/installer/context.rs +++ b/liana-gui/src/installer/context.rs @@ -5,6 +5,7 @@ use std::time::Duration; use crate::{ app::settings::KeySetting, + backup::Backup, lianalite::client::backend::{BackendClient, BackendWalletClient}, node::bitcoind::{Bitcoind, InternalBitcoindConfig}, signer::Signer, @@ -69,6 +70,7 @@ pub struct Context { pub internal_bitcoind_config: Option, pub internal_bitcoind: Option, pub remote_backend: RemoteBackend, + pub backup: Option, } impl Context { @@ -95,6 +97,7 @@ impl Context { internal_bitcoind_config: None, internal_bitcoind: None, remote_backend, + backup: None, } } } diff --git a/liana-gui/src/installer/mod.rs b/liana-gui/src/installer/mod.rs index 4c0a6144..2eee9ed2 100644 --- a/liana-gui/src/installer/mod.rs +++ b/liana-gui/src/installer/mod.rs @@ -66,7 +66,7 @@ pub struct Installer { signer: Arc>, /// Context is data passed through each step. - context: Context, + pub context: Context, } impl Installer { diff --git a/liana-gui/src/loader.rs b/liana-gui/src/loader.rs index 72f0a1c9..e785f721 100644 --- a/liana-gui/src/loader.rs +++ b/liana-gui/src/loader.rs @@ -23,6 +23,9 @@ use lianad::{ StartupError, }; +use crate::app; +use crate::backup::Backup; +use crate::export::RestoreBackupError; use crate::{ app::{ cache::Cache, @@ -56,7 +59,7 @@ pub struct Loader { pub daemon_started: bool, pub internal_bitcoind: Option, pub waiting_daemon_bitcoind: bool, - + pub backup: Option, step: Step, } @@ -83,6 +86,20 @@ pub enum Message { Cache, Arc, Option, + Option, + ), + Error, + >, + ), + App( + Result< + ( + Cache, + Arc, + app::Config, + Arc, + PathBuf, + Option, ), Error, >, @@ -100,6 +117,7 @@ impl Loader { gui_config: GUIConfig, network: bitcoin::Network, internal_bitcoind: Option, + backup: Option, ) -> (Self, Task) { let path = gui_config .daemon_rpc_path @@ -114,6 +132,7 @@ impl Loader { daemon_started: false, internal_bitcoind, waiting_daemon_bitcoind: false, + backup, }, Task::perform(connect(path), Message::Loaded), ) @@ -134,6 +153,7 @@ impl Loader { self.datadir_path.clone(), self.network, self.internal_bitcoind.clone(), + self.backup.clone(), ), Message::Synced, ); @@ -232,6 +252,7 @@ impl Loader { self.datadir_path.clone(), self.network, self.internal_bitcoind.clone(), + self.backup.clone(), ), Message::Synced, ); @@ -290,6 +311,7 @@ impl Loader { self.gui_config.clone(), self.network, self.internal_bitcoind.clone(), + self.backup.clone(), ); *self = loader; cmd @@ -391,12 +413,14 @@ pub async fn load_application( datadir_path: PathBuf, network: bitcoin::Network, internal_bitcoind: Option, + backup: Option, ) -> Result< ( Arc, Cache, Arc, Option, + Option, ), Error, > { @@ -421,7 +445,7 @@ pub async fn load_application( ..Default::default() }; - Ok((Arc::new(wallet), cache, daemon, internal_bitcoind)) + Ok((Arc::new(wallet), cache, daemon, internal_bitcoind, backup)) } #[derive(Clone, Debug)] @@ -582,6 +606,7 @@ pub enum Error { Daemon(DaemonError), Bitcoind(StartInternalBitcoindError), BitcoindLogs(std::io::Error), + RestoreBackup(RestoreBackupError), } impl std::fmt::Display for Error { @@ -592,6 +617,7 @@ impl std::fmt::Display for Error { Self::Daemon(e) => write!(f, "Liana daemon error: {}", e), Self::Bitcoind(e) => write!(f, "Bitcoind error: {}", e), Self::BitcoindLogs(e) => write!(f, "Bitcoind logs error: {}", e), + Self::RestoreBackup(e) => write!(f, "Restore backup: {e}"), } } } diff --git a/liana-gui/src/main.rs b/liana-gui/src/main.rs index e256be2c..476d3ca6 100644 --- a/liana-gui/src/main.rs +++ b/liana-gui/src/main.rs @@ -24,11 +24,12 @@ use lianad::config::Config as DaemonConfig; use liana_gui::{ app::{self, cache::Cache, config::default_datadir, wallet::Wallet, App}, datadir, + export::import_backup_at_launch, hw::HardwareWalletConfig, installer::{self, Installer}, launcher::{self, Launcher}, lianalite::{ - client::{backend::api, backend::BackendWalletClient}, + client::backend::{api, BackendWalletClient}, login, }, loader::{self, Loader}, @@ -162,7 +163,7 @@ impl GUI { network, log_level.unwrap_or_else(|| cfg.log_level().unwrap_or(LevelFilter::INFO)), ); - let (loader, command) = Loader::new(datadir_path, cfg, network, None); + let (loader, command) = Loader::new(datadir_path, cfg, network, None, None); cmds.push(command.map(|msg| Message::Load(Box::new(msg)))); State::Loader(Box::new(loader)) } @@ -242,12 +243,13 @@ impl GUI { self.state = State::Login(Box::new(login)); command.map(|msg| Message::Login(Box::new(msg))) } else { - let (loader, command) = Loader::new(datadir_path, cfg, network, None); + let (loader, command) = + Loader::new(datadir_path, cfg, network, None, None); self.state = State::Loader(Box::new(loader)); command.map(|msg| Message::Load(Box::new(msg))) } } else { - let (loader, command) = Loader::new(datadir_path, cfg, network, None); + let (loader, command) = Loader::new(datadir_path, cfg, network, None, None); self.state = State::Loader(Box::new(loader)); command.map(|msg| Message::Load(Box::new(msg))) } @@ -334,6 +336,7 @@ impl GUI { cfg, daemon_cfg.bitcoin_config.network, internal_bitcoind, + i.context.backup.take(), ); self.state = State::Loader(Box::new(loader)); command.map(|msg| Message::Load(Box::new(msg))) @@ -353,18 +356,45 @@ impl GUI { self.state = State::Launcher(Box::new(launcher)); command.map(|msg| Message::Launch(Box::new(msg))) } - loader::Message::Synced(Ok((wallet, cache, daemon, bitcoind))) => { - let (app, command) = App::new( - cache, - wallet, - loader.gui_config.clone(), - daemon, - loader.datadir_path.clone(), - bitcoind, - ); + loader::Message::Synced(Ok((wallet, cache, daemon, bitcoind, backup))) => { + if let Some(backup) = backup { + let config = loader.gui_config.clone(); + let datadir = loader.datadir_path.clone(); + Task::perform( + async move { + import_backup_at_launch( + cache, wallet, config, daemon, datadir, bitcoind, backup, + ) + .await + }, + |r| { + let r = r.map_err(loader::Error::RestoreBackup); + Message::Load(Box::new(loader::Message::App(r))) + }, + ) + } else { + let (app, command) = App::new( + cache, + wallet, + loader.gui_config.clone(), + daemon, + loader.datadir_path.clone(), + bitcoind, + ); + self.state = State::App(app); + command.map(|msg| Message::Run(Box::new(msg))) + } + } + loader::Message::App(Ok((cache, wallet, config, daemon, datadir, bitcoind))) => { + let (app, command) = App::new(cache, wallet, config, daemon, datadir, bitcoind); self.state = State::App(app); command.map(|msg| Message::Run(Box::new(msg))) } + loader::Message::App(Err(e)) => { + tracing::error!("Fail to import backup: {e}"); + Task::none() + } + _ => loader.update(*msg).map(|msg| Message::Load(Box::new(msg))), }, (State::App(i), Message::Run(msg)) => { From aa53c3e44e375656b0b9d3076167c82f2204273e Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Wed, 12 Mar 2025 13:07:38 +0100 Subject: [PATCH 32/44] import: implement wallet_from_backup() --- liana-gui/src/app/state/export.rs | 7 ++- liana-gui/src/export.rs | 88 ++++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/liana-gui/src/app/state/export.rs b/liana-gui/src/app/state/export.rs index 7de48c93..7f9944ca 100644 --- a/liana-gui/src/app/state/export.rs +++ b/liana-gui/src/app/state/export.rs @@ -49,6 +49,7 @@ impl ExportModal { ImportExportType::ImportPsbt => "Import PSBT", ImportExportType::ImportDescriptor => "Import Descriptor", ImportExportType::ImportBackup(..) => "Restore Backup", + ImportExportType::WalletFromBackup => "Import existing wallet from backup", } } @@ -71,9 +72,12 @@ impl ExportModal { ImportExportType::ImportPsbt => "psbt.psbt".into(), ImportExportType::ImportDescriptor => "descriptor.descriptor".into(), ImportExportType::ExportLabels => format!("liana-labels-{date}.jsonl"), - ImportExportType::ExportBackup(_) | ImportExportType::ImportBackup(_, _) => { + ImportExportType::ExportBackup(_) => { format!("liana-backup-{date}.json") } + ImportExportType::WalletFromBackup | ImportExportType::ImportBackup(_, _) => { + "liana-backup.json".to_string() + } } } @@ -132,6 +136,7 @@ impl ExportModal { ImportExportMessage::UpdateAliases(map.clone()).into() }); } + Progress::WalletFromBackup(_) => {} }, ImportExportMessage::TimedOut => { self.stop(ImportExportState::TimedOut); diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index c50b5bdd..91479803 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -16,7 +16,7 @@ use async_hwi::bitbox::api::btc::Fingerprint; use chrono::{DateTime, Duration, Utc}; use liana::{ descriptors::LianaDescriptor, - miniscript::bitcoin::{Amount, Psbt, Txid}, + miniscript::bitcoin::{Amount, Network, Psbt, Txid}, }; use lianad::{ bip329::{error::ExportError, Labels}, @@ -195,6 +195,7 @@ pub enum ImportExportType { ExportLabels, ImportPsbt, ImportDescriptor, + WalletFromBackup, } impl ImportExportType { @@ -207,6 +208,7 @@ impl ImportExportType { | ImportExportType::ExportLabels => "Export successful!", ImportExportType::ImportBackup(_, _) | ImportExportType::ImportPsbt + | ImportExportType::WalletFromBackup | ImportExportType::ImportDescriptor => "Import successful", } } @@ -270,6 +272,14 @@ pub enum Progress { LabelsConflict(SyncSender), KeyAliasesConflict(SyncSender), UpdateAliases(HashMap), + WalletFromBackup( + ( + LianaDescriptor, + Network, + HashMap, + Backup, + ), + ), } pub struct Export { @@ -305,6 +315,7 @@ impl Export { path: PathBuf, ) { match export_type { + ImportExportType::WalletFromBackup => wallet_from_backup(sender, path).await, ImportExportType::Transactions => export_transactions(sender, daemon, path).await, ImportExportType::ExportPsbt(str) => export_string(sender, path, str), ImportExportType::Descriptor(descriptor) => export_descriptor(sender, path, descriptor), @@ -1021,6 +1032,81 @@ impl From for RestoreBackupError { } } +/// Create a wallet from a backup +/// - load backup from file +/// - extract descriptor +/// - extract network +/// - extract aliases +pub async fn wallet_from_backup(sender: Sender, path: PathBuf) { + // Load backup from file + let mut file = open_file_read!(path, sender); + + let mut backup_str = String::new(); + if let Err(e) = file.read_to_string(&mut backup_str) { + send_error!(sender, e.into()); + return; + } + + let backup: Result = serde_json::from_str(&backup_str); + let backup = match backup { + Ok(psbt) => psbt, + Err(e) => { + send_error!(sender, Error::BackupImport(format!("{:?}", e))); + return; + } + }; + + let network = backup.network; + + let account = match backup.accounts.len() { + 0 => { + send_error!( + sender, + Error::BackupImport("There is no account in the backup!".into()) + ); + return; + } + 1 => backup.accounts.first().expect("already checked"), + _ => { + send_error!( + sender, + Error::BackupImport( + "Liana is actually not supporting import of backup with several accounts!" + .into() + ) + ); + return; + } + }; + + let descriptor = match LianaDescriptor::from_str(&account.descriptor) { + Ok(d) => d, + Err(_) => { + send_error!( + sender, + Error::BackupImport( + "The backup descriptor is not a valid Liana descriptor!".into() + ) + ); + return; + } + }; + + let mut aliases: HashMap = HashMap::new(); + for (k, v) in &account.keys { + if let Some(alias) = &v.alias { + aliases.insert(*k, alias.clone()); + } + } + + send_progress!( + sender, + WalletFromBackup((descriptor, network, aliases, backup)) + ); + send_progress!(sender, Progress(100.0)); + send_progress!(sender, Ended); +} + #[allow(unused)] /// Import backup data if wallet created from a backup /// - check if networks matches From 091557a8dfd03788cfaa202ad87c2dd15e1e347f Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Tue, 11 Mar 2025 19:17:30 +0100 Subject: [PATCH 33/44] installer: add existing wallet from backup --- liana-gui/src/export.rs | 18 +++-- liana-gui/src/installer/message.rs | 12 ++-- liana-gui/src/installer/mod.rs | 5 ++ .../src/installer/step/descriptor/mod.rs | 71 ++++++++++++++++--- liana-gui/src/installer/view/mod.rs | 56 +++++++++++++-- 5 files changed, 138 insertions(+), 24 deletions(-) diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index 91479803..1234825c 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -191,11 +191,11 @@ pub enum ImportExportType { Option>, /*overwrite_labels*/ Option>, /*overwrite_aliases*/ ), + WalletFromBackup, Descriptor(LianaDescriptor), ExportLabels, ImportPsbt, ImportDescriptor, - WalletFromBackup, } impl ImportExportType { @@ -276,7 +276,7 @@ pub enum Progress { ( LianaDescriptor, Network, - HashMap, + HashMap, Backup, ), ), @@ -315,7 +315,6 @@ impl Export { path: PathBuf, ) { match export_type { - ImportExportType::WalletFromBackup => wallet_from_backup(sender, path).await, ImportExportType::Transactions => export_transactions(sender, daemon, path).await, ImportExportType::ExportPsbt(str) => export_string(sender, path, str), ImportExportType::Descriptor(descriptor) => export_descriptor(sender, path, descriptor), @@ -324,6 +323,7 @@ impl Export { ImportExportType::ImportDescriptor => import_descriptor(sender, path), ImportExportType::ExportBackup(str) => export_string(sender, path, str), ImportExportType::ImportBackup(..) => import_backup(sender, path, daemon).await, + ImportExportType::WalletFromBackup => wallet_from_backup(sender, path).await, }; } @@ -1092,10 +1092,16 @@ pub async fn wallet_from_backup(sender: Sender, path: PathBuf) { } }; - let mut aliases: HashMap = HashMap::new(); + let mut aliases: HashMap = HashMap::new(); for (k, v) in &account.keys { - if let Some(alias) = &v.alias { - aliases.insert(*k, alias.clone()); + if let Some(ks) = KeySetting::from_backup( + v.alias.clone().unwrap_or("".into()), + *k, + v.role, + v.key_type, + v.metadata.clone(), + ) { + aliases.insert(*k, ks); } } diff --git a/liana-gui/src/installer/message.rs b/liana-gui/src/installer/message.rs index 81ebc225..efe5dc2f 100644 --- a/liana-gui/src/installer/message.rs +++ b/liana-gui/src/installer/message.rs @@ -2,13 +2,15 @@ use liana::miniscript::{ bitcoin::{bip32::Fingerprint, Network}, DescriptorPublicKey, }; -use std::path::PathBuf; +use std::{collections::HashMap, path::PathBuf}; use super::{context, Error}; use crate::{ - app::settings::ProviderKey, - app::view::Close, - backup, + app::{ + settings::{self, ProviderKey}, + view::Close, + }, + backup::{self, Backup}, download::{DownloadError, Progress}, export::ImportExportMessage, hw::HardwareWalletMessage, @@ -55,6 +57,8 @@ pub enum Message { BackupWallet, ExportWallet(Result), ImportExport(ImportExportMessage), + ImportBackup, + WalletFromBackup((HashMap, Backup)), } impl Close for Message { diff --git a/liana-gui/src/installer/mod.rs b/liana-gui/src/installer/mod.rs index 2eee9ed2..245121cf 100644 --- a/liana-gui/src/installer/mod.rs +++ b/liana-gui/src/installer/mod.rs @@ -292,6 +292,11 @@ impl Installer { .expect("There is always a step") .update(&mut self.hws, Message::Installed(Err(e))) } + Message::WalletFromBackup((ks, backup)) => { + self.context.keys = ks; + self.context.backup = Some(backup); + Task::none() + } _ => self .steps .get_mut(self.current) diff --git a/liana-gui/src/installer/step/descriptor/mod.rs b/liana-gui/src/installer/step/descriptor/mod.rs index c1dcd91e..866fb447 100644 --- a/liana-gui/src/installer/step/descriptor/mod.rs +++ b/liana-gui/src/installer/step/descriptor/mod.rs @@ -18,7 +18,7 @@ use async_hwi::DeviceKind; use crate::{ app::{settings::KeySetting, state::export::ExportModal, wallet::wallet_name}, backup::{self, Backup}, - export::{ImportExportMessage, ImportExportType}, + export::{ImportExportMessage, ImportExportType, Progress}, hw::{HardwareWallet, HardwareWallets}, installer::{ message::{self, Message}, @@ -32,6 +32,8 @@ pub struct ImportDescriptor { imported_descriptor: form::Value, wrong_network: bool, error: Option, + modal: Option, + imported_backup: bool, } impl ImportDescriptor { @@ -41,6 +43,8 @@ impl ImportDescriptor { imported_descriptor: form::Value::default(), wrong_network: false, error: None, + modal: None, + imported_backup: false, } } @@ -77,14 +81,55 @@ impl Step for ImportDescriptor { fn skip(&self, ctx: &Context) -> bool { ctx.remote_backend.is_some() } - // 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 subscription(&self, _hws: &HardwareWallets) -> Subscription { + if let Some(modal) = &self.modal { + if let Some(sub) = modal.subscription() { + sub.map(|m| Message::ImportExport(ImportExportMessage::Progress(m))) + } else { + Subscription::none() + } + } else { + Subscription::none() + } + } + fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Task { - if let Message::DefineDescriptor(message::DefineDescriptor::ImportDescriptor(desc)) = - message - { - self.imported_descriptor.value = desc; - self.check_descriptor(self.network); + match message { + Message::DefineDescriptor(message::DefineDescriptor::ImportDescriptor(desc)) => { + self.imported_descriptor.value = desc; + self.check_descriptor(self.network); + } + Message::ImportExport(ImportExportMessage::Close) => { + self.modal = None; + } + Message::ImportBackup => { + if !self.imported_backup { + let modal = ExportModal::new(None, ImportExportType::WalletFromBackup); + let launch = modal.launch(false); + self.modal = Some(modal); + return launch; + } + } + Message::ImportExport(ImportExportMessage::Progress(Progress::WalletFromBackup(r))) => { + let (descriptor, network, aliases, backup) = r; + if self.network == network { + self.imported_backup = true; + self.imported_descriptor.value = descriptor.to_string(); + return Task::perform(async move { (aliases, backup) }, |(a, b)| { + Message::WalletFromBackup((a, b)) + }); + } else { + self.error = Some("Backup network do not match the selected network!".into()); + } + } + Message::ImportExport(m) => { + if let Some(modal) = self.modal.as_mut() { + let task: Task = modal.update(m); + return task; + }; + } + _ => {} } Task::none() } @@ -108,13 +153,19 @@ impl Step for ImportDescriptor { progress: (usize, usize), email: Option<&'a str>, ) -> Element { - view::import_descriptor( + let content = view::import_descriptor( progress, email, &self.imported_descriptor, + self.imported_backup, self.wrong_network, self.error.as_ref(), - ) + ); + if let Some(modal) = &self.modal { + modal.view(content) + } else { + content + } } } diff --git a/liana-gui/src/installer/view/mod.rs b/liana-gui/src/installer/view/mod.rs index 733df5b9..ba405730 100644 --- a/liana-gui/src/installer/view/mod.rs +++ b/liana-gui/src/installer/view/mod.rs @@ -264,11 +264,15 @@ pub fn import_descriptor<'a>( progress: (usize, usize), email: Option<&'a str>, imported_descriptor: &form::Value, + imported_backup: bool, wrong_network: bool, error: Option<&String>, ) -> Element<'a, Message> { + let valid = !imported_descriptor.value.is_empty() && imported_descriptor.valid; + let col_descriptor = Column::new() .push(text("Descriptor:").bold()) + .push(Space::with_height(10)) .push( form::Form::new_trimmed("Descriptor", imported_descriptor, |msg| { Message::DefineDescriptor(message::DefineDescriptor::ImportDescriptor(msg)) @@ -280,21 +284,65 @@ pub fn import_descriptor<'a>( }) .size(text::P1_SIZE) .padding(10), + ); + + let descriptor = if imported_backup { + None + } else { + Some(col_descriptor) + }; + + let or = if !valid && !imported_backup { + Some( + Row::new() + .push(text("or").bold()) + .push(Space::with_width(Length::Fill)), ) - .spacing(10); + } else { + None + }; + + let import_backup = if !valid && !imported_backup { + Some( + Row::new() + .push(button::secondary(None, "Import backup").on_press(Message::ImportBackup)) + .push(Space::with_width(Length::Fill)), + ) + } else { + None + }; + + let backup_imported = if imported_backup { + Some( + Row::new() + .push(text("Backup successfuly imported!").bold()) + .push(Space::with_width(Length::Fill)), + ) + } else { + None + }; + layout( progress, email, "Import the wallet", Column::new() - .push(Column::new().spacing(20).push(col_descriptor).push(text( - "If you are using a Bitcoin Core node, \ + .push( + Column::new() + .spacing(20) + .push_maybe(descriptor) + .push_maybe(or) + .push_maybe(import_backup) + .push_maybe(backup_imported) + .push(text( + "If you are using a Bitcoin Core node, \ you will need to perform a rescan of \ the blockchain after creating the wallet \ in order to see your coins and past \ transactions. This can be done in \ Settings > Node.", - ))) + )), + ) .push( if imported_descriptor.value.is_empty() || !imported_descriptor.valid { button::secondary(None, "Next").width(Length::Fixed(200.0)) From 107e81071c7e60157fb3d00f90e3e3258c213aff Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Mon, 17 Mar 2025 08:00:01 +0100 Subject: [PATCH 34/44] lianad; update docs & tests for getinfo command --- doc/API.md | 2 ++ tests/test_rpc.py | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/doc/API.md b/doc/API.md index 0be165b2..c7f3d754 100644 --- a/doc/API.md +++ b/doc/API.md @@ -63,6 +63,8 @@ This command does not take any parameter for now. | `rescan_progress` | float or null | Progress of an ongoing rescan as a percentage (between 0 and 1) if there is any | | `timestamp` | integer | Unix timestamp of wallet creation date | | `last_poll_timestamp`| integer or null | Unix timestamp of last poll (if any) of the blockchain | +| `receive_index` | integer | Last index used to generate a receive address | +| `change_index` | integer | Last index used to generate a change address | ### `getnewaddress` diff --git a/tests/test_rpc.py b/tests/test_rpc.py index 4ba57726..c46e5a9a 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -36,6 +36,8 @@ def test_getinfo(lianad): time.sleep(lianad.poll_interval_secs + 1) res = lianad.rpc.getinfo() assert res["last_poll_timestamp"] > last_poll_timestamp + assert res["receive_index"] == 0 + assert res["change_index"] == 0 def test_getaddress(lianad): @@ -45,6 +47,9 @@ def test_getaddress(lianad): assert res["address"] != lianad.rpc.getnewaddress()["address"] # new address has derivation_index higher than the previous one assert lianad.rpc.getnewaddress()["derivation_index"] == res["derivation_index"] + 2 + info = lianad.rpc.getinfo() + assert info["receive_index"] == res["derivation_index"] + 3 + assert info["change_index"] == 0 def test_listaddresses(lianad): @@ -420,6 +425,8 @@ def test_create_spend(lianad, bitcoind): assert len(spend_psbt.o) == 4 assert len(spend_psbt.tx.vout) == 4 + assert lianad.rpc.getinfo()["change_index"] == 15 + # The transaction must contain the spent transaction for each input for P2WSH. But not for Taproot. # We don't make assumptions about the ordering of PSBT inputs. if USE_TAPROOT: From e1b90b056fe6a339acd4f9726cc10813d96bc741 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Mon, 17 Mar 2025 12:36:57 +0100 Subject: [PATCH 35/44] lianad: in DaemonControl::update_deriv_indexes() limit the gap the index can be incremented and return db indexes --- liana-gui/src/daemon/client/mod.rs | 4 +- liana-gui/src/daemon/embedded.rs | 3 +- liana-gui/src/daemon/mod.rs | 3 +- liana-gui/src/lianalite/client/backend/mod.rs | 4 +- lianad/src/commands/mod.rs | 59 ++++++++++++++----- lianad/src/jsonrpc/api.rs | 5 +- 6 files changed, 54 insertions(+), 24 deletions(-) diff --git a/liana-gui/src/daemon/client/mod.rs b/liana-gui/src/daemon/client/mod.rs index c0f69263..53651d36 100644 --- a/liana-gui/src/daemon/client/mod.rs +++ b/liana-gui/src/daemon/client/mod.rs @@ -5,7 +5,7 @@ use std::path::Path; use async_trait::async_trait; use lianad::bip329::Labels; -use lianad::commands::GetLabelsBip329Result; +use lianad::commands::{GetLabelsBip329Result, UpdateDerivIndexesResult}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -87,7 +87,7 @@ impl Daemon for Lianad { &self, receive: Option, change: Option, - ) -> Result<(), DaemonError> { + ) -> Result { self.call("updatederivationindexes", Some(vec![receive, change])) } diff --git a/liana-gui/src/daemon/embedded.rs b/liana-gui/src/daemon/embedded.rs index 96e63613..94935ab3 100644 --- a/liana-gui/src/daemon/embedded.rs +++ b/liana-gui/src/daemon/embedded.rs @@ -1,4 +1,5 @@ use lianad::bip329::Labels; +use lianad::commands::UpdateDerivIndexesResult; use std::collections::{HashMap, HashSet}; use std::path::Path; use tokio::sync::Mutex; @@ -102,7 +103,7 @@ impl Daemon for EmbeddedDaemon { &self, receive: Option, change: Option, - ) -> Result<(), DaemonError> { + ) -> Result { self.command(|daemon| { daemon .update_deriv_indexes(receive, change) diff --git a/liana-gui/src/daemon/mod.rs b/liana-gui/src/daemon/mod.rs index e2706271..5a9e9755 100644 --- a/liana-gui/src/daemon/mod.rs +++ b/liana-gui/src/daemon/mod.rs @@ -15,6 +15,7 @@ use liana::miniscript::bitcoin::{ address, bip32::Fingerprint, psbt::Psbt, secp256k1, Address, Network, OutPoint, Txid, }; use lianad::bip329::Labels; +use lianad::commands::UpdateDerivIndexesResult; use lianad::{ commands::{CoinStatus, LabelItem, TransactionInfo}, config::Config, @@ -90,7 +91,7 @@ pub trait Daemon: Debug { &self, receive: Option, change: Option, - ) -> Result<(), DaemonError>; + ) -> Result; async fn list_coins( &self, statuses: &[CoinStatus], diff --git a/liana-gui/src/lianalite/client/backend/mod.rs b/liana-gui/src/lianalite/client/backend/mod.rs index e6095090..7b1ffaf2 100644 --- a/liana-gui/src/lianalite/client/backend/mod.rs +++ b/liana-gui/src/lianalite/client/backend/mod.rs @@ -14,7 +14,7 @@ use liana::{ }; use lianad::{ bip329::Labels, - commands::{CoinStatus, GetInfoDescriptors, LCSpendInfo, LabelItem}, + commands::{CoinStatus, GetInfoDescriptors, LCSpendInfo, LabelItem, UpdateDerivIndexesResult}, config::Config, }; use reqwest::{Error, IntoUrl, Method, RequestBuilder, Response}; @@ -631,7 +631,7 @@ impl Daemon for BackendWalletClient { &self, _receive: Option, _change: Option, - ) -> Result<(), DaemonError> { + ) -> Result { Err(DaemonError::NotImplemented) } diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index 1c9dabde..2417f246 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -362,32 +362,53 @@ impl DaemonControl { &self, receive: Option, change: Option, - ) -> Result<(), CommandError> { + ) -> Result { let mut db_conn = self.db.connection(); + const MAX_INCREMENT_GAP: u32 = 1_000; + + let db_receive = db_conn.receive_index().into(); + let mut final_receive = db_receive; + + let db_change = db_conn.change_index().into(); + let mut final_change = db_change; + if let Some(index) = receive { - let child = match ChildNumber::from_normal_idx(index) { - Ok(i) => i, - Err(_) => return Err(CommandError::InvalidDerivationIndex), - }; - let db_receive = db_conn.receive_index(); - if child > db_receive { - db_conn.set_receive_index(child, &self.secp); + ChildNumber::from_normal_idx(index) + .map_err(|_| CommandError::InvalidDerivationIndex)?; + if index > db_receive { + let delta = (index - db_receive).min(MAX_INCREMENT_GAP); + let index = db_receive + delta; + final_receive = index; + match ChildNumber::from_normal_idx(index) { + Ok(i) => { + db_conn.set_receive_index(i, &self.secp); + } + Err(_) => return Err(CommandError::InvalidDerivationIndex), + }; } } if let Some(index) = change { - let child = match ChildNumber::from_normal_idx(index) { - Ok(i) => i, - Err(_) => return Err(CommandError::InvalidDerivationIndex), - }; - let db_change = db_conn.change_index(); - if child > db_change { - db_conn.set_change_index(child, &self.secp); + ChildNumber::from_normal_idx(index) + .map_err(|_| CommandError::InvalidDerivationIndex)?; + if index > db_change { + let delta = (index - db_change).min(MAX_INCREMENT_GAP); + let index = db_change + delta; + final_change = index; + match ChildNumber::from_normal_idx(index) { + Ok(i) => { + db_conn.set_change_index(i, &self.secp); + } + Err(_) => return Err(CommandError::InvalidDerivationIndex), + }; } } - Ok(()) + Ok(UpdateDerivIndexesResult { + receive: final_receive, + change: final_change, + }) } /// list addresses @@ -1215,6 +1236,12 @@ pub struct GetInfoResult { pub change_index: u32, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpdateDerivIndexesResult { + pub receive: u32, + pub change: u32, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GetAddressResult { #[serde(deserialize_with = "deser_addr_assume_checked")] diff --git a/lianad/src/jsonrpc/api.rs b/lianad/src/jsonrpc/api.rs index 29a0ce85..808156a1 100644 --- a/lianad/src/jsonrpc/api.rs +++ b/lianad/src/jsonrpc/api.rs @@ -238,8 +238,9 @@ fn update_deriv_indexes( None => None, }; - control.update_deriv_indexes(receive, change)?; - Ok(serde_json::json!({})) + Ok(serde_json::json!( + control.update_deriv_indexes(receive, change)? + )) } fn list_confirmed(control: &DaemonControl, params: Params) -> Result { From 9417549ddb5e6ffb51e668228e6faf8d18937db5 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Mon, 17 Mar 2025 16:09:11 +0100 Subject: [PATCH 36/44] lianad: docs & tests for updatederivationindexes command --- doc/API.md | 33 ++++++++++++- tests/test_rpc.py | 123 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 1 deletion(-) diff --git a/doc/API.md b/doc/API.md index c7f3d754..ead31e98 100644 --- a/doc/API.md +++ b/doc/API.md @@ -9,8 +9,9 @@ Commands must be sent as valid JSONRPC 2.0 requests, ending with a `\n`. | ----------------------------------------------------------- | ---------------------------------------------------- | | [`stop`](#stop) | Stops liana daemon | | [`getinfo`](#getinfo) | Get general information about the daemon | +| [`updatederivationindexes`](#updatederivationindexes) | Update last generated addresses derivation indexes | | [`getnewaddress`](#getnewaddress) | Get a new receiving address | -| [`listaddresses`](#listaddresses) | List addresses given start_index and count | +| [`listaddresses`](#listaddresses) | List addresses given start_index and count | | [`listcoins`](#listcoins) | List all wallet transaction outputs. | | [`createspend`](#createspend) | Create a new Spend transaction | | [`updatespend`](#updatespend) | Store a created Spend transaction | @@ -66,6 +67,36 @@ This command does not take any parameter for now. | `receive_index` | integer | Last index used to generate a receive address | | `change_index` | integer | Last index used to generate a change address | + +### `updatederivationindexes` + +Updates the last generated address derivation indexes in the wallet database. +At least one of the `receive` or `change` arguments is required. + +Derivation indexes **must be unhardened**. If a provided index is lower than +the one currently stored in the database, it will be ignored. + +**Note:** Each time a derivation index in the database is incremented, the +corresponding new addresses must be inserted into the database. To prevent +excessive increments, there is a limit: the derivation index can only be +incremented by a maximum of **1000** from its current value. + +The updated indexes will be returned in the response. + +#### Request + +| Field | Type | Description | +|-----------|-------------------|----------------------------------------------------------| +| `receive` | integer(optional) | The latest receive address derivation index to update | +| `change` | integer(optional) | The latest change address derivation index to update | + +#### Response + +| Field | Type | Description | +|-----------|---------|----------------------------------------------------------| +| `receive` | integer | The updated receive address derivation index | +| `change` | integer | The updated change address derivation index | + ### `getnewaddress` Get a new address for receiving coins. This will always generate a new address regardless of whether diff --git a/tests/test_rpc.py b/tests/test_rpc.py index c46e5a9a..4b7a4a3c 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -20,6 +20,8 @@ from test_framework.utils import ( USE_TAPROOT, ) +MAX_DERIV = 2**31 - 1 + def test_getinfo(lianad): res = lianad.rpc.getinfo() @@ -40,6 +42,127 @@ def test_getinfo(lianad): assert res["change_index"] == 0 +def test_update_derivation_indexes(lianad): + info = lianad.rpc.getinfo() + assert info["receive_index"] == 0 + assert info["change_index"] == 0 + + ret = lianad.rpc.updatederivationindexes(0, 0) + info = lianad.rpc.getinfo() + assert info["receive_index"] == 0 + assert info["change_index"] == 0 + assert ret["receive"] == 0 + assert ret["change"] == 0 + + ret = lianad.rpc.updatederivationindexes(receive=3) + info = lianad.rpc.getinfo() + assert info["receive_index"] == 3 + assert info["change_index"] == 0 + assert ret["receive"] == 3 + assert ret["change"] == 0 + + ret = lianad.rpc.updatederivationindexes(change=4) + info = lianad.rpc.getinfo() + assert info["receive_index"] == 3 + assert info["change_index"] == 4 + assert ret["receive"] == 3 + assert ret["change"] == 4 + + ret = lianad.rpc.updatederivationindexes(receive=1, change=2) + info = lianad.rpc.getinfo() + assert info["receive_index"] == 3 + assert info["change_index"] == 4 + assert ret["receive"] == 3 + assert ret["change"] == 4 + + ret = lianad.rpc.updatederivationindexes(5, 6) + info = lianad.rpc.getinfo() + assert info["receive_index"] == 5 + assert info["change_index"] == 6 + assert ret["receive"] == 5 + assert ret["change"] == 6 + + ret = lianad.rpc.updatederivationindexes(0, 0) + info = lianad.rpc.getinfo() + assert info["receive_index"] == 5 + assert info["change_index"] == 6 + assert ret["receive"] == 5 + assert ret["change"] == 6 + + # Will explicitly error on invalid indexes + with pytest.raises( + RpcError, + match=re.escape( + "Invalid params: Invalid value for \'receive\' param" + ), + ): + lianad.rpc.updatederivationindexes(-1) + + with pytest.raises( + RpcError, + match=re.escape( + "Invalid params: Invalid value for \'change\' param" + ), + ): + lianad.rpc.updatederivationindexes(0, -1) + + with pytest.raises( + RpcError, + match=re.escape( + "Unhardened or overflowing BIP32 derivation index." + ), + ): + lianad.rpc.updatederivationindexes(MAX_DERIV + 1, 2) + + with pytest.raises( + RpcError, + match=re.escape( + "Unhardened or overflowing BIP32 derivation index." + ), + ): + lianad.rpc.updatederivationindexes(0, MAX_DERIV + 1) + + with pytest.raises( + RpcError, + match=re.escape( + "Unhardened or overflowing BIP32 derivation index." + ), + ): + lianad.rpc.updatederivationindexes(receive=(MAX_DERIV+1)) + + with pytest.raises( + RpcError, + match=re.escape( + "Unhardened or overflowing BIP32 derivation index." + ), + ): + lianad.rpc.updatederivationindexes(change=(MAX_DERIV+1)) + + with pytest.raises( + RpcError, + match=re.escape( + "Invalid params: Missing \'receive\' or \'change\' parameter" + ), + ): + lianad.rpc.updatederivationindexes() + + last_derivs = lianad.rpc.updatederivationindexes(0, 0) + last_receive = last_derivs["receive"] + last_change = last_derivs["change"] + + ret = lianad.rpc.updatederivationindexes(0, (MAX_DERIV - 1)) + assert ret["receive"] == last_receive + assert ret["change"] == last_change + 1000 + + last_derivs = lianad.rpc.updatederivationindexes(0, 0) + last_receive = last_derivs["receive"] + last_change = last_derivs["change"] + + ret = lianad.rpc.updatederivationindexes((MAX_DERIV -1 ), 0) + assert ret["receive"] == last_receive + 1000 + assert ret["change"] == last_change + + def test_getaddress(lianad): res = lianad.rpc.getnewaddress() assert "address" in res From 2291138fb64d5be7d66e24e72cbfb11af6a55915 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Mon, 17 Mar 2025 20:02:22 +0100 Subject: [PATCH 37/44] lianad: docs & tests for getlabelsbip329 command --- doc/API.md | 20 ++++++++++++++ tests/test_rpc.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/doc/API.md b/doc/API.md index ead31e98..a7b0f9ff 100644 --- a/doc/API.md +++ b/doc/API.md @@ -25,6 +25,7 @@ Commands must be sent as valid JSONRPC 2.0 requests, ending with a `\n`. | [`createrecovery`](#createrecovery) | Create a recovery transaction to sweep expired coins | | [`updatelabels`](#updatelabels) | Update the labels | | [`getlabels`](#getlabels) | Get the labels for the given addresses, txids and outpoints | +| [`getlabelsbip329`](#getlabelsbip329) | Get the labels in BIP-0329 format | # Reference @@ -460,3 +461,22 @@ Items without labels are not present in the response map. | Field | Type | Description | | -------- | ------ | -------------------------------------------------------------------------------- | | `labels` | object | A mapping of bitcoin addresses, txids and outpoints as keys, and string as values | + +### `getlabelsbip329` + +Retrieve a list of labels in [BIP-0329](https://github.com/bitcoin/bips/blob/master/bip-0329.mediawiki) +format, with pagination support. + +#### Request + +| Field | Type | Description | +| -------- | ------- | ------------------------------------------ | +| `offset` | integer | Index to start returning labels from | +| `limit` | integer | Maximum number of labels to return | + +#### Response + +| Field | Type | Description | +| -------- | ------ | ------------------------------------------------- | +| `labels` | array | A list of BIP-0329-formatted label objects | + diff --git a/tests/test_rpc.py b/tests/test_rpc.py index 4b7a4a3c..39ea5b82 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -1211,6 +1211,76 @@ def test_labels(lianad, bitcoind): assert res[random_address] == "this address is random" +def test_labels_bip329(lianad, bitcoind): + # Label 5 addresses + addresses = [] + for i in range(0,5): + addr = lianad.rpc.getnewaddress()["address"] + addresses.append(addr) + lianad.rpc.updatelabels({addr: f"addr{i}"}) + + # Label 5 coin + txids = [] + for i in range(0,5): + addr = lianad.rpc.getnewaddress()["address"] + txid = bitcoind.rpc.sendtoaddress(addr, 1) + txids.append(txid) + wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == i+1 ) + + coins = lianad.rpc.listcoins()["coins"] + for i in range(0,5): + coin = coins[i] + lianad.rpc.updatelabels({coin["outpoint"]: f"coin{i}"}) + + # Label 5 transactions + for i, txid in enumerate(txids): + lianad.rpc.updatelabels({txid: f"tx{i}"}) + + # Get Bip-0329 labels + bip329_labels = lianad.rpc.getlabelsbip329(0,100)["labels"] + assert len(bip329_labels) == 15 + + def label_found(name, labels): + for label in labels: + if label["label"] == name: + return True + return False + + # All transactions are labelled + for i in range(0, len(txids)): + assert label_found(f"tx{i}", bip329_labels) + + # All adresses are labelled + for i in range(0, len(addresses)): + assert label_found(f"addr{i}", bip329_labels) + + # All coins are labelled + for i in range(0, len(coins)): + assert label_found(f"coin{i}", bip329_labels) + + # There is no conflict between batches + batch1 = lianad.rpc.getlabelsbip329(0,5)["labels"] + assert len(batch1) == 5 + + batch2 = lianad.rpc.getlabelsbip329(5,5)["labels"] + assert len(batch2) == 5 + + batch3 = lianad.rpc.getlabelsbip329(10,5)["labels"] + assert len(batch3) == 5 + + for label in batch1: + print(label) + name = label["label"] + + assert not label_found(name, batch2) + assert not label_found(name, batch3) + + for label in batch2: + name = label["label"] + assert not label_found(name, batch1) + assert not label_found(name, batch3) + + def test_rbfpsbt_bump_fee(lianad, bitcoind): """Test the use of RBF to bump the fee of a transaction.""" From cac7bce60aedc5097bbdf9ffb1184688f4bcb48a Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Mon, 17 Mar 2025 20:10:14 +0100 Subject: [PATCH 38/44] installer: minor UI fixes --- liana-gui/src/app/state/export.rs | 8 ++++---- liana-gui/src/app/view/export.rs | 8 ++++++-- liana-gui/src/app/view/settings.rs | 14 +++++++------- liana-gui/src/installer/prompt.rs | 4 ++-- liana-gui/src/installer/view/mod.rs | 19 ++++++++++++------- liana-ui/src/icon.rs | 8 ++++++++ 6 files changed, 39 insertions(+), 22 deletions(-) diff --git a/liana-gui/src/app/state/export.rs b/liana-gui/src/app/state/export.rs index 7f9944ca..2dbee270 100644 --- a/liana-gui/src/app/state/export.rs +++ b/liana-gui/src/app/state/export.rs @@ -104,15 +104,15 @@ impl ExportModal { } Progress::Finished | Progress::Ended => self.state = ImportExportState::Ended, Progress::KeyAliasesConflict(ref sender) => { - if let ImportExportType::ImportBackup(_, aliases) = &self.import_export_type { + if let ImportExportType::ImportBackup(_, None) = &self.import_export_type { self.import_export_type = - ImportExportType::ImportBackup(Some(sender.clone()), aliases.clone()); + ImportExportType::ImportBackup(None, Some(sender.clone())); } } Progress::LabelsConflict(ref sender) => { - if let ImportExportType::ImportBackup(labels, _) = &self.import_export_type { + if let ImportExportType::ImportBackup(None, _) = &self.import_export_type { self.import_export_type = - ImportExportType::ImportBackup(labels.clone(), Some(sender.clone())); + ImportExportType::ImportBackup(Some(sender.clone()), None); } } Progress::Error(e) => { diff --git a/liana-gui/src/app/view/export.rs b/liana-gui/src/app/view/export.rs index 16a57490..0e20fa36 100644 --- a/liana-gui/src/app/view/export.rs +++ b/liana-gui/src/app/view/export.rs @@ -92,7 +92,7 @@ pub fn export_modal<'a, Message: From + Clone + 'a>( .width(Length::Fill) }); - let p = match state { + let mut p = match state { ImportExportState::Init => 0.0, ImportExportState::ChoosePath | ImportExportState::Path(_) | ImportExportState::Started => { 5.0 @@ -103,6 +103,10 @@ pub fn export_modal<'a, Message: From + Clone + 'a>( | ImportExportState::Ended | ImportExportState::Closed => 100.0, }; + // keep progress bar visible + if p == 0.0 { + p += 2.5; + } let progress_bar_row = Row::new() .push(Space::with_width(30)) .push(progress_bar(0.0..=100.0, p)) @@ -120,6 +124,6 @@ pub fn export_modal<'a, Message: From + Clone + 'a>( .push(Space::with_height(5)), ) .width(Length::Fixed(500.0)) - .height(Length::Fixed(220.0)) + .height(Length::Fixed(250.0)) .into() } diff --git a/liana-gui/src/app/view/settings.rs b/liana-gui/src/app/view/settings.rs index 6835ea7c..6de7a004 100644 --- a/liana-gui/src/app/view/settings.rs +++ b/liana-gui/src/app/view/settings.rs @@ -167,35 +167,35 @@ pub fn import_export<'a>(cache: &'a Cache, warning: Option<&Error>) -> Element<' let export_descriptor = settings_section( "Export descriptor", None, - icon::wallet_icon(), + icon::backup_icon(), Message::Settings(SettingsMessage::ExportDescriptor), ); let export_transactions = settings_section( "Export transactions", None, - icon::wallet_icon(), + icon::backup_icon(), Message::Settings(SettingsMessage::ExportTransactions), ); let export_labels = settings_section( "Export labels", None, - icon::wallet_icon(), + icon::backup_icon(), Message::Settings(SettingsMessage::ExportLabels), ); let export_wallet = settings_section( "Back Up Wallet", None, - icon::wallet_icon(), + icon::backup_icon(), Message::Settings(SettingsMessage::ExportWallet), ); let import_wallet = settings_section( "Restore wallet", None, - icon::wallet_icon(), + icon::restore_icon(), Message::Settings(SettingsMessage::ImportWallet), ); @@ -920,12 +920,12 @@ pub fn wallet_settings<'a>( let import_export = Row::new() .push( - button::secondary(Some(icon::wallet_icon()), "Backup") + button::secondary(Some(icon::backup_icon()), "Backup") .on_press(Message::Settings(SettingsMessage::ExportWallet)), ) .push(Space::with_width(10)) .push( - button::secondary(Some(icon::wallet_icon()), "Restore") + button::secondary(Some(icon::restore_icon()), "Restore") .on_press(Message::Settings(SettingsMessage::ImportWallet)), ) .push(Space::with_width(Length::Fill)); diff --git a/liana-gui/src/installer/prompt.rs b/liana-gui/src/installer/prompt.rs index de1c44e1..bf4f2c79 100644 --- a/liana-gui/src/installer/prompt.rs +++ b/liana-gui/src/installer/prompt.rs @@ -1,5 +1,5 @@ -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 to know both 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 back up your private key, this is your mnemonic ('seed words'). For finding the coins that belong to you, you back up a template of your Script (your 'addresses'), this is your descriptor. However, note the descriptor need not be stored as securely as the private key. A thief who steals your descriptor but not your private key cannot steal your funds."; +pub const BACKUP_DESCRIPTOR_MESSAGE: &str = "A backup of your wallet configuration is necessary to recover your funds. Please make sure to store your Wallet backup file (or alternatively to copy and paste the descriptor string) in one or more secure and accessible locations. You still need to back up your seed phrases too, since they are not included in the file."; +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 to know both 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 back up your private key, this is your mnemonic ('seed words'). For finding the coins that belong to you, you back up a template of your Script (your 'addresses'), this is your descriptor, included in your wallet backup file. However, note the descriptor need not be stored as securely as the private key. A thief who steals your descriptor but not your private key cannot steal your funds."; pub const DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP: &str = "The alias is applied on all the keys derived from the same seed"; pub const REGISTER_DESCRIPTOR_HELP: &str = "To be used with the wallet, a signing device needs the descriptor. If the descriptor contains one or more keys imported from an external signing device, the descriptor must be registered on it. Registration confirms that the device is able to handle the policy. Registration on a device is not a substitute for backing up the descriptor."; diff --git a/liana-gui/src/installer/view/mod.rs b/liana-gui/src/installer/view/mod.rs index ba405730..7aea026e 100644 --- a/liana-gui/src/installer/view/mod.rs +++ b/liana-gui/src/installer/view/mod.rs @@ -740,10 +740,17 @@ pub fn backup_descriptor<'a>( error: Option<&Error>, done: bool, ) -> Element<'a, Message> { + let backup_button = if done { + button::secondary(Some(icon::backup_icon()), "Back Up Wallet") + .on_press(Message::BackupWallet) + } else { + button::primary(Some(icon::backup_icon()), "Back Up Wallet").on_press(Message::BackupWallet) + }; + layout( progress, email, - "Backup your wallet descriptor", + "Back Up your wallet", Column::new() .push( Column::new() @@ -793,10 +800,7 @@ pub fn backup_descriptor<'a>( .push( Row::new() .push(Space::with_width(Length::Fill)) - .push( - button::secondary(Some(icon::wallet_icon()), "Backup") - .on_press(Message::BackupWallet), - ) + .push(backup_button) .push(Space::with_width(10)) .push( button::secondary(Some(icon::clipboard_icon()), "Copy") @@ -813,10 +817,11 @@ pub fn backup_descriptor<'a>( .max_width(1500), ) .push( - checkbox("I have backed up my descriptor", done).on_toggle(Message::UserActionDone), + checkbox("I have backed up my wallet/descriptor", done) + .on_toggle(Message::UserActionDone), ) .push(if done { - button::secondary(None, "Next") + button::primary(None, "Next") .on_press(Message::Next) .width(Length::Fixed(200.0)) } else { diff --git a/liana-ui/src/icon.rs b/liana-ui/src/icon.rs index c733734a..9a8dd1c5 100644 --- a/liana-ui/src/icon.rs +++ b/liana-ui/src/icon.rs @@ -123,6 +123,14 @@ pub fn round_key_icon() -> Text<'static> { bootstrap_icon('\u{F44E}') } +pub fn backup_icon() -> Text<'static> { + bootstrap_icon('\u{F356}') +} + +pub fn restore_icon() -> Text<'static> { + bootstrap_icon('\u{F358}') +} + const ICONEX_ICONS: Font = Font::with_name("Untitled1"); fn iconex_icon(unicode: char) -> Text<'static> { From 4e17483340971b1f3ff4674385843aa1bc068000 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Tue, 18 Mar 2025 12:10:29 +0100 Subject: [PATCH 39/44] lianad: reverse the byte order of txid before parsing bip329 label transactions --- lianad/src/database/sqlite/schema.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lianad/src/database/sqlite/schema.rs b/lianad/src/database/sqlite/schema.rs index 0eafed73..34eb5fd2 100644 --- a/lianad/src/database/sqlite/schema.rs +++ b/lianad/src/database/sqlite/schema.rs @@ -9,7 +9,7 @@ use miniscript::bitcoin::{ bip32, consensus::encode, psbt::Psbt, - Address, OutPoint, + Address, OutPoint, Txid, }; // Due to limitations of Sqlite's ALTER TABLE command and in order not to recreate @@ -376,7 +376,11 @@ impl From for DbLabelledKind { impl From for Label { fn from(value: DbLabel) -> Self { - let ref_ = value.item; + let mut ref_ = value.item; + if value.item_kind == DbLabelledKind::Txid { + let frontward: Txid = bitcoin::consensus::encode::deserialize_hex(&ref_).unwrap(); + ref_ = frontward.to_string(); + } let label = if value.value.is_empty() { None } else { From 2e6cceea96783b85c6e7654cfc7600d63c86133c Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Tue, 18 Mar 2025 13:20:59 +0100 Subject: [PATCH 40/44] backup: rename Key::metadata in Key::proprietary --- liana-gui/src/app/settings.rs | 4 ++-- liana-gui/src/backup.rs | 2 +- liana-gui/src/export.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/liana-gui/src/app/settings.rs b/liana-gui/src/app/settings.rs index 7657de98..b2b5aba7 100644 --- a/liana-gui/src/app/settings.rs +++ b/liana-gui/src/app/settings.rs @@ -185,7 +185,7 @@ impl KeySetting { alias: Some(self.name.clone()), role: None, key_type: Some(KeyType::ThirdParty), - metadata, + proprietary: metadata, }; } } @@ -194,7 +194,7 @@ impl KeySetting { alias: Some(self.name.clone()), role: None, key_type: None, - metadata: serde_json::Value::Null, + proprietary: serde_json::Value::Null, } } diff --git a/liana-gui/src/backup.rs b/liana-gui/src/backup.rs index d66d9c9d..636153ef 100644 --- a/liana-gui/src/backup.rs +++ b/liana-gui/src/backup.rs @@ -460,7 +460,7 @@ pub struct Key { #[serde(default, skip_serializing_if = "Option::is_none")] pub key_type: Option, #[serde(default, skip_serializing_if = "Value::is_null")] - pub metadata: Value, + pub proprietary: Value, } #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index 1234825c..be32cfe8 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -960,7 +960,7 @@ pub async fn import_backup( *k, v.role, v.key_type, - v.metadata.clone(), + v.proprietary.clone(), ) { settings_aliases.insert(*k, ks); } @@ -1099,7 +1099,7 @@ pub async fn wallet_from_backup(sender: Sender, path: PathBuf) { *k, v.role, v.key_type, - v.metadata.clone(), + v.proprietary.clone(), ) { aliases.insert(*k, ks); } From 1abe503431892c45b9459849f4ac94b814c67e71 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Tue, 18 Mar 2025 15:09:43 +0100 Subject: [PATCH 41/44] backup: update local state when alias updated from a backup restore --- liana-gui/src/app/state/settings/mod.rs | 19 +------------------ liana-gui/src/app/state/settings/wallet.rs | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/liana-gui/src/app/state/settings/mod.rs b/liana-gui/src/app/state/settings/mod.rs index 774de791..e8f625e4 100644 --- a/liana-gui/src/app/state/settings/mod.rs +++ b/liana-gui/src/app/state/settings/mod.rs @@ -1,9 +1,9 @@ mod bitcoind; mod wallet; +use std::convert::From; use std::path::PathBuf; use std::sync::Arc; -use std::{collections::HashMap, convert::From}; use iced::Task; @@ -223,23 +223,6 @@ impl State for ImportExportSettingsState { self.modal = None; } Message::View(view::Message::ImportExport(m)) => { - if let ImportExportMessage::UpdateAliases(aliases) = m { - let mut wallet = (*self.wallet).clone(); - let mut ka = HashMap::new(); - let mut pk = HashMap::new(); - aliases.iter().for_each(|(k, ks)| { - ka.insert(*k, ks.name()); - if let Some(p_key) = &ks.provider_key { - pk.insert(*k, p_key.clone()); - } - }); - wallet.keys_aliases = ka; - wallet.provider_keys = pk; - let wallet = Arc::new(wallet); - return Task::perform(async {}, move |_| { - Message::WalletUpdated(Ok(wallet.clone())) - }); - } if let Some(modal) = self.modal.as_mut() { return modal.update(m); }; diff --git a/liana-gui/src/app/state/settings/wallet.rs b/liana-gui/src/app/state/settings/wallet.rs index 6db4930d..1bf7c387 100644 --- a/liana-gui/src/app/state/settings/wallet.rs +++ b/liana-gui/src/app/state/settings/wallet.rs @@ -203,6 +203,22 @@ impl State for WalletSettingsState { Task::none() } + Message::View(view::Message::ImportExport(ImportExportMessage::UpdateAliases( + aliases, + ))) => { + self.processing = true; + self.updated = false; + Task::perform( + update_keys_aliases( + self.data_dir.clone(), + cache.network, + self.wallet.clone(), + aliases.into_iter().map(|(fg, ks)| (fg, ks.name)).collect(), + daemon, + ), + Message::WalletUpdated, + ) + } Message::View(view::Message::ImportExport(ImportExportMessage::Close)) => { if let Modal::ImportExport(_) = &self.modal { self.modal = Modal::None; @@ -472,7 +488,7 @@ async fn register_wallet( Ok(wallet) } -async fn update_keys_aliases( +pub async fn update_keys_aliases( data_dir: PathBuf, network: Network, wallet: Arc, From df8d98b275bbf0aa96729f46afc53a24728ed60e Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Tue, 18 Mar 2025 18:38:51 +0100 Subject: [PATCH 42/44] gui: use Display instead Debug to print ExportModal errors --- liana-gui/src/app/view/export.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/liana-gui/src/app/view/export.rs b/liana-gui/src/app/view/export.rs index 0e20fa36..6c707985 100644 --- a/liana-gui/src/app/view/export.rs +++ b/liana-gui/src/app/view/export.rs @@ -33,7 +33,7 @@ pub fn export_modal<'a, Message: From + Clone + 'a>( .map(Container::new); let msg = if let Some(error) = error { - format!("{:?}", error) + format!("{}", error) } else { match state { ImportExportState::Init => "".to_string(), From 477e914801f3d9c0b06be56617a5dc2f3273d136 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Wed, 19 Mar 2025 07:52:13 +0100 Subject: [PATCH 43/44] export: remove NotImplemented from RestoreBackupError --- liana-gui/src/export.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index be32cfe8..fa8bc9aa 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -995,7 +995,6 @@ pub enum RestoreBackupError { LianaConnectNotSupported, GetLabels, LabelsNotEmpty, - NotImplemented, InvalidPsbt, } @@ -1020,7 +1019,6 @@ impl Display for RestoreBackupError { f, "Cannot load labels: there is already labels into the database" ), - RestoreBackupError::NotImplemented => write!(f, "Not implemented"), RestoreBackupError::InvalidPsbt => write!(f, "Psbt is invalid"), } } From f8ca6f123fd56ac71a2a3bffe9568c0ecb5a9b4d Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Wed, 19 Mar 2025 09:38:33 +0100 Subject: [PATCH 44/44] backup: add ser/de test & minors changes --- liana-gui/src/backup.rs | 75 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 8 deletions(-) diff --git a/liana-gui/src/backup.rs b/liana-gui/src/backup.rs index 636153ef..cab64de3 100644 --- a/liana-gui/src/backup.rs +++ b/liana-gui/src/backup.rs @@ -43,14 +43,14 @@ fn now() -> u64 { .as_secs() } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct Backup { #[serde(default, skip_serializing_if = "Option::is_none")] pub name: Option, - #[serde(default)] pub accounts: Vec, pub network: Network, - pub date: u64, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub date: Option, /// App proprietary metadata (settings, configuration, etc..) #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")] pub proprietary: serde_json::Map, @@ -171,7 +171,7 @@ impl Backup { accounts: vec![account], network: ctx.network, proprietary: serde_json::Map::new(), - date: now, + date: Some(now), version: 0, }) } @@ -270,7 +270,7 @@ impl Backup { accounts: vec![account], network, proprietary: serde_json::Map::new(), - date: now(), + date: Some(now()), version: 0, }) } @@ -371,7 +371,7 @@ async fn get_transactions( Ok(vec) } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct Account { #[serde(default, skip_serializing_if = "Option::is_none")] pub name: Option, @@ -398,13 +398,13 @@ pub struct Account { pub proprietary: serde_json::Map, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct ChainTip { pub block_height: i32, pub block_hash: Option, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct Coin { amount: u64, outpoint: String, @@ -484,3 +484,62 @@ pub enum KeyType { /// Service the user pay for ThirdParty, } + +#[cfg(test)] +mod test { + use super::*; + + fn round_trip(backup: &Backup) -> bool { + let serialized = serde_json::to_string(backup).unwrap(); + let parsed: Backup = serde_json::from_str(&serialized).unwrap(); + *backup == parsed + } + + #[test] + fn backup_serde() { + let mut backup = Backup { + name: None, + accounts: Vec::new(), + network: Network::Signet, + date: Some(0), + proprietary: serde_json::Map::new(), + version: 0, + }; + let serialized = serde_json::to_string(&backup).unwrap(); + let expected = r#"{"accounts":[],"network":"signet","date":0,"version":0}"#; + assert_eq!(expected, serialized); + assert!(round_trip(&backup)); + + backup.name = Some("Liana".into()); + + let serialized = serde_json::to_string(&backup).unwrap(); + let expected = r#"{"name":"Liana","accounts":[],"network":"signet","date":0,"version":0}"#; + assert_eq!(expected, serialized); + assert!(round_trip(&backup)); + + let descr_str = r#"wsh(or_d(pk([19608592/48'/1'/0'/2']tpubDEjf1AbrUjxnw8jg6Gi12CunPqnCobLP6Ktoy4Hd52pa65d6QRPg5CSkdFrqPDjJ8BAUuMEDVDRQVjtuWWksMqBeZCqyABFucN9ErQq8oVX/<0;1>/*),and_v(v:pkh([19608592/48'/1'/0'/2']tpubDEjf1AbrUjxnw8jg6Gi12CunPqnCobLP6Ktoy4Hd52pa65d6QRPg5CSkdFrqPDjJ8BAUuMEDVDRQVjtuWWksMqBeZCqyABFucN9ErQq8oVX/<2;3>/*),older(52596))))#x6u6lmej"#.to_string(); + + let account = Account::new(descr_str); + backup.accounts.push(account); + + let serialized = serde_json::to_string(&backup).unwrap(); + println!("{serialized}"); + let expected = r#"{"name":"Liana","accounts":[{"descriptor":"wsh(or_d(pk([19608592/48'/1'/0'/2']tpubDEjf1AbrUjxnw8jg6Gi12CunPqnCobLP6Ktoy4Hd52pa65d6QRPg5CSkdFrqPDjJ8BAUuMEDVDRQVjtuWWksMqBeZCqyABFucN9ErQq8oVX/<0;1>/*),and_v(v:pkh([19608592/48'/1'/0'/2']tpubDEjf1AbrUjxnw8jg6Gi12CunPqnCobLP6Ktoy4Hd52pa65d6QRPg5CSkdFrqPDjJ8BAUuMEDVDRQVjtuWWksMqBeZCqyABFucN9ErQq8oVX/<2;3>/*),older(52596))))#x6u6lmej"}],"network":"signet","date":0,"version":0}"#; + assert_eq!(expected, serialized); + assert!(round_trip(&backup)); + + // if there is no version, the default is 0 + let no_version = r#"{"name":"Liana","accounts":[{"descriptor":"wsh(or_d(pk([19608592/48'/1'/0'/2']tpubDEjf1AbrUjxnw8jg6Gi12CunPqnCobLP6Ktoy4Hd52pa65d6QRPg5CSkdFrqPDjJ8BAUuMEDVDRQVjtuWWksMqBeZCqyABFucN9ErQq8oVX/<0;1>/*),and_v(v:pkh([19608592/48'/1'/0'/2']tpubDEjf1AbrUjxnw8jg6Gi12CunPqnCobLP6Ktoy4Hd52pa65d6QRPg5CSkdFrqPDjJ8BAUuMEDVDRQVjtuWWksMqBeZCqyABFucN9ErQq8oVX/<2;3>/*),older(52596))))#x6u6lmej"}],"network":"signet","date":0}"#; + let parsed: Backup = serde_json::from_str(no_version).unwrap(); + assert_eq!(parsed.version, 0); + + // Network is mandatory for an account + let no_network = r#"{"name":"Liana","accounts":[{"descriptor":"wsh(or_d(pk([19608592/48'/1'/0'/2']tpubDEjf1AbrUjxnw8jg6Gi12CunPqnCobLP6Ktoy4Hd52pa65d6QRPg5CSkdFrqPDjJ8BAUuMEDVDRQVjtuWWksMqBeZCqyABFucN9ErQq8oVX/<0;1>/*),and_v(v:pkh([19608592/48'/1'/0'/2']tpubDEjf1AbrUjxnw8jg6Gi12CunPqnCobLP6Ktoy4Hd52pa65d6QRPg5CSkdFrqPDjJ8BAUuMEDVDRQVjtuWWksMqBeZCqyABFucN9ErQq8oVX/<2;3>/*),older(52596))))#x6u6lmej"}],"date":0,"version":0}"#; + let parsed: Result = serde_json::from_str(no_network); + assert!(parsed.is_err()); + + // But it's the only mandatory field, w/ accounts array + let minimal = r#"{"network":"signet","accounts":[]}"#; + let _parsed: Backup = serde_json::from_str(minimal).unwrap(); + } +}