From 2f7d3bbaec191c780df56cb84a6f224f616e1e07 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Tue, 10 Dec 2024 04:21:56 +0100 Subject: [PATCH] gui(export): do not use a separate function for export logic --- liana-gui/src/app/export.rs | 345 +++++++++++++++++------------- liana-gui/src/app/state/export.rs | 8 +- 2 files changed, 204 insertions(+), 149 deletions(-) diff --git a/liana-gui/src/app/export.rs b/liana-gui/src/app/export.rs index a82d6f646..c95ba5b8e 100644 --- a/liana-gui/src/app/export.rs +++ b/liana-gui/src/app/export.rs @@ -1,4 +1,5 @@ use std::{ + collections::HashMap, fs::{self, File}, io::Write, path::PathBuf, @@ -6,19 +7,23 @@ use std::{ mpsc::{channel, Receiver, Sender}, Arc, Mutex, }, - time, + time::{self, Duration as TDuration}, }; use chrono::{DateTime, Duration, Utc}; -use liana::miniscript::bitcoin::{Amount, Denomination::Bitcoin}; -use lianad::commands::LabelItem; +use liana::miniscript::bitcoin::{Amount, Txid}; use tokio::{ - runtime::Runtime, task::{JoinError, JoinHandle}, time::sleep, }; -use crate::daemon::{model::Labelled, Daemon, DaemonError}; +use crate::{ + daemon::{ + model::{HistoryTransaction, Labelled, TransactionKind}, + Daemon, DaemonBackend, DaemonError, + }, + lianalite::client::backend::api::DEFAULT_LIMIT, +}; macro_rules! send_error { ($sender:ident, $error:ident) => { @@ -33,6 +38,23 @@ macro_rules! send_error { }; } +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); + } + }; + ($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(ExportProgress::Ended) { +// tracing::error!("ExportState::start() fail to send msg: {}", e); +// } + #[derive(Debug, Clone)] pub enum Error { Io(String), @@ -42,6 +64,7 @@ pub enum Error { ChannelLost, NoParentDir, Daemon(String), + TxTimeMissing, } impl From for Error { @@ -73,39 +96,28 @@ pub enum State { pub enum ExportProgress { Started(Arc>>), Progress(f32), - Ended(Box), + Ended, Finnished, Error(Error), None, } -#[derive(Debug)] -pub enum ExportType { - Transactions, -} - pub struct ExportState { pub receiver: Receiver, pub sender: Option>, pub handle: Option>>>, pub daemon: Arc, - pub export_type: ExportType, pub path: Box, } impl ExportState { - pub fn new( - daemon: Arc, - export_type: ExportType, - path: Box, - ) -> Self { + pub fn new(daemon: Arc, path: Box) -> Self { let (sender, receiver) = channel(); ExportState { receiver, sender: Some(sender), handle: None, daemon, - export_type, path, } } @@ -114,19 +126,186 @@ impl ExportState { if let (true, Some(sender)) = (self.handle.is_none(), self.sender.take()) { let daemon = self.daemon.clone(); let path = self.path.clone(); - let function = self.function(); let cloned_sender = sender.clone(); let handle = tokio::spawn(async move { - function(sender, daemon, path); + 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; + } + + let info = daemon.get_info().await; + let start = match info { + Ok(info) => info.timestamp, + Err(e) => { + 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(start, 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(0.1)); + } + + let (start, max) = match daemon.backend() { + DaemonBackend::RemoteBackend => (0, DEFAULT_LIMIT as u64), + _ => (start, 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(start, 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 fetch + 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.first().expect("checked").time { + t + } else { + send_error!(sender, TxTimeMissing); + return; + }; + // limit too low, all tx are in the same timestamp + // whe 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| a.compare(b)); + + 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 = 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 addr = if let TransactionKind::IncomingSinglePayment(outpoint) = tx.kind { + tx.coins.get(&outpoint).map(|c| c.address.to_string()) + } else { + None + }; + let mut label = if let Some(txid) = txid_label { + txid + } else if let Some(addr) = addr { + addr + } else if tx.is_send_to_self() { + "self send".to_string() + } else { + "".to_string() + }; + if !label.is_empty() { + label = format!("\"{}\"", label); + } + let fee = tx.fee_amount.unwrap_or(Amount::ZERO).to_btc(); + let value = tx.incoming_amount.to_btc() - tx.outgoing_amount.to_btc() - fee; + let txid = tx.txid.to_string(); + let block = tx.height.map(|h| h.to_string()).unwrap_or("".to_string()); + + 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(1.0)); + send_progress!(sender, Ended); }); let handle = Arc::new(Mutex::new(handle)); // we send the handle to the GUI so we can kill the thread on timeout // or user cancel action - if let Err(e) = cloned_sender.send(ExportProgress::Started(handle.clone())) { - tracing::error!("ExportState::start fail to send msg: {}", e); - } + send_progress!(cloned_sender, Started(handle.clone())); self.handle = Some(handle); } else { tracing::error!("ExportState can start only once!"); @@ -140,14 +319,6 @@ impl ExportState { _ => unreachable!(), } } - - pub fn function( - &self, - ) -> impl Fn(Sender, Arc, Box) { - match self.export_type { - ExportType::Transactions => export_transactions, - } - } } pub async fn export_subscription(mut state: ExportState) -> (ExportProgress, ExportState) { @@ -188,118 +359,6 @@ pub async fn export_subscription(mut state: ExportState) -> (ExportProgress, Exp (ExportProgress::None, state) } -pub fn export_transactions( - sender: Sender, - daemon: Arc, - path: Box, -) { - log::info!("export_transactions()"); - 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".to_string(); - if let Err(e) = file.write_all(header.as_bytes()) { - send_error!(sender, e.into()); - return; - } - log::info!("export_transactions() header written"); - - let rt = Runtime::new().unwrap(); - let info = rt.block_on(daemon.get_info()); - - let start = match info { - Ok(info) => info.timestamp, - Err(e) => { - send_error!(sender, e.into()); - return; - } - }; - // look 2 hour forward - let end = ((Utc::now() + Duration::hours(2)).timestamp()) as u32; - - let history = rt.block_on(daemon.list_history_txs(start, end, u64::MAX)); - let txs = match history { - Ok(h) => h, - Err(e) => { - send_error!(sender, e.into()); - return; - } - }; - log::info!("export_transactions() history received"); - - for 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 = str.replace("T", " "); - str[0..(str.len() - 6)].to_string() - }) - .unwrap_or("".to_string()); - - let labels = tx.labelled(); - let txid = labels - .iter() - .filter(|l| matches!(l, LabelItem::Txid(_))) - .collect::>() - .first() - .map(|l| l.to_string()); - let addr = labels - .iter() - .filter(|l| matches!(l, LabelItem::Address(_))) - .collect::>() - .first() - .map(|l| l.to_string()); - let outpoint = labels - .iter() - .filter(|l| matches!(l, LabelItem::Txid(_))) - .collect::>() - .first() - .map(|l| l.to_string()); - let mut label = txid.unwrap_or(addr.unwrap_or(outpoint.unwrap_or("".to_string()))); - if !label.is_empty() { - label = format!("\"{}\"", label); - } - let fee = tx.fee_amount.unwrap_or(Amount::ZERO); - let value = (tx.incoming_amount - tx.outgoing_amount - fee).to_string_in(Bitcoin); - let txid = tx.txid.to_string(); - let block = tx.height.map(|h| h.to_string()).unwrap_or("".to_string()); - - 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; - } - } - log::info!("export_transactions() written"); - - if let Err(e) = sender.send(ExportProgress::Ended(path)) { - tracing::error!("ExportState::start() fail to send msg: {}", e); - } -} - pub async fn get_path() -> Option { rfd::AsyncFileDialog::new() .set_title("Choose a location to export...") diff --git a/liana-gui/src/app/state/export.rs b/liana-gui/src/app/state/export.rs index 9f83b8ecd..27aa78aa1 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::{ - export::{self, get_path, ExportProgress, ExportType}, + export::{self, get_path, ExportProgress}, message::Message, view::{self, export::export_modal}, }; @@ -137,11 +137,7 @@ impl ExportModal { ExportState::Started | ExportState::Progress(_) => { Some(iced::subscription::unfold( "transactions", - export::ExportState::new( - self.daemon.clone(), - ExportType::Transactions, - Box::new(path.to_path_buf()), - ), + export::ExportState::new(self.daemon.clone(), Box::new(path.to_path_buf())), export::export_subscription, )) }