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) } }