import: implement import from file for psbt & descriptor

This commit is contained in:
pythcoiner 2025-01-30 07:22:18 +01:00
parent 2b9324993a
commit 158651ebe7
No known key found for this signature in database
GPG Key ID: C1048AEEDF303B88
7 changed files with 194 additions and 115 deletions

View File

@ -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<HashMap<String, Option<String>>, Error>),
BroadcastModal(Result<HashSet<Txid>, Error>),
RbfModal(Box<HistoryTransaction>, bool, Result<HashSet<Txid>, Error>),
Export(ExportMessage),
Export(ImportExportMessage),
}

View File

@ -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<PathBuf>,
handle: Option<Arc<Mutex<JoinHandle<()>>>>,
state: ExportState,
state: ImportExportState,
error: Option<export::Error>,
daemon: Arc<dyn Daemon + Sync + Send>,
export_type: ExportType,
import_export_type: ImportExportType,
}
impl ExportModal {
#[allow(clippy::new_without_default)]
pub fn new(daemon: Arc<dyn Daemon + Sync + Send>, export_type: ExportType) -> Self {
pub fn new(daemon: Arc<dyn Daemon + Sync + Send>, 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<app::message::Message> {
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<app::message::Message> {
pub fn update(&mut self, message: ImportExportMessage) -> Task<app::message::Message> {
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<Subscription<export::ExportProgress>> {
pub fn subscription(&self) -> Option<Subscription<export::Progress>> {
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(),
),
))
}

View File

@ -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,
)))
});
}
}

View File

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

View File

@ -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)]

View File

@ -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()

View File

@ -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<PathBuf>),
Close,
}
impl From<ExportMessage> for view::Message {
fn from(value: ExportMessage) -> Self {
Self::Export(value)
impl From<ImportExportMessage> 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<JoinError> for Error {
@ -156,29 +162,31 @@ pub enum Status {
}
#[derive(Debug, Clone)]
pub enum ExportProgress {
pub enum Progress {
Started(Arc<Mutex<JoinHandle<()>>>),
Progress(f32),
Ended,
Finished,
Error(Error),
None,
Psbt(Psbt),
Descriptor(LianaDescriptor),
}
pub struct Export {
pub receiver: Receiver<ExportProgress>,
pub sender: Option<Sender<ExportProgress>>,
pub receiver: Receiver<Progress>,
pub sender: Option<Sender<Progress>>,
pub handle: Option<Arc<Mutex<JoinHandle<()>>>>,
pub daemon: Arc<dyn Daemon + Sync + Send>,
pub daemon: Option<Arc<dyn Daemon + Sync + Send>>,
pub path: Box<PathBuf>,
pub export_type: ExportType,
pub export_type: ImportExportType,
}
impl Export {
pub fn new(
daemon: Arc<dyn Daemon + Sync + Send>,
daemon: Option<Arc<dyn Daemon + Sync + Send>>,
path: Box<PathBuf>,
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<ExportProgress>,
daemon: Arc<dyn Daemon + Sync + Send>,
export_type: ImportExportType,
sender: Sender<Progress>,
daemon: Option<Arc<dyn Daemon + Sync + Send>>,
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<dyn Daemon + Sync + Send>,
path: PathBuf,
export_type: ExportType,
) -> impl Stream<Item = ExportProgress> {
export_type: ImportExportType,
) -> impl Stream<Item = Progress> {
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<ExportProgress>,
daemon: Arc<dyn Daemon + Sync + Send>,
sender: Sender<Progress>,
daemon: Option<Arc<dyn Daemon + Sync + Send>>,
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<ExportProgress>,
path: PathBuf,
descriptor: LianaDescriptor,
) {
pub fn export_descriptor(sender: Sender<Progress>, 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<ExportProgress>, path: PathBuf, psbt: String) {
pub fn export_psbt(sender: Sender<Progress>, 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<ExportProgress>, path: PathBuf, psbt: String)
send_progress!(sender, Ended);
}
pub fn import_psbt(sender: Sender<Progress>, 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<Progress>, 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<PathBuf> {
rfd::AsyncFileDialog::new()
.set_title("Choose a location to export...")