From f13cd1fe73f2fc1afda0a4f35cac7f4840e5c7c8 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Wed, 22 Jan 2025 04:33:53 +0100 Subject: [PATCH] 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");