export: rename 'State' into 'Export' and separate export logic

This commit is contained in:
pythcoiner 2025-01-22 04:33:53 +01:00
parent 52e552313f
commit f13cd1fe73
No known key found for this signature in database
GPG Key ID: C1048AEEDF303B88
2 changed files with 219 additions and 175 deletions

View File

@ -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,

View File

@ -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<JoinError> 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<ExportProgress>,
pub sender: Option<Sender<ExportProgress>>,
pub handle: Option<Arc<Mutex<JoinHandle<()>>>>,
pub daemon: Arc<dyn Daemon + Sync + Send>,
pub path: Box<PathBuf>,
pub export_type: ExportType,
}
impl State {
pub fn new(daemon: Arc<dyn Daemon + Sync + Send>, path: Box<PathBuf>) -> Self {
impl Export {
pub fn new(
daemon: Arc<dyn Daemon + Sync + Send>,
path: Box<PathBuf>,
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<ExportProgress>,
daemon: Arc<dyn Daemon + Sync + Send>,
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::<Txid, HistoryTransaction>::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<dyn Daemon + Sync + Send>,
path: PathBuf,
export_type: ExportType,
) -> impl Stream<Item = ExportProgress> {
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<ExportProgress>,
daemon: Arc<dyn Daemon + Sync + Send>,
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::<Txid, HistoryTransaction>::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<PathBuf> {
let date = chrono::Local::now().format("%Y-%m-%dT%H-%M-%S");
let file_name = format!("liana-txs-{date}.csv");