Skip to content

Commit

Permalink
gui: add and export transactions feature
Browse files Browse the repository at this point in the history
  • Loading branch information
pythcoiner committed Dec 23, 2024
1 parent ecce76a commit 22f4875
Show file tree
Hide file tree
Showing 12 changed files with 1,155 additions and 23 deletions.
469 changes: 466 additions & 3 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions liana-gui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ base64 = "0.21"
bitcoin_hashes = "0.12"
reqwest = { version = "0.11", default-features=false, features = ["json", "rustls-tls"] }
rust-ini = "0.19.0"
rfd = "0.15.1"


[target.'cfg(windows)'.dependencies]
Expand Down
2 changes: 2 additions & 0 deletions liana-gui/src/app/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use lianad::config::Config as DaemonConfig;
use crate::{
app::{cache::Cache, error::Error, view, wallet::Wallet},
daemon::model::*,
export::ExportMessage,
hw::HardwareWalletMessage,
};

Expand Down Expand Up @@ -46,4 +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),
}
130 changes: 130 additions & 0 deletions liana-gui/src/app/state/export.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
use std::{
path::PathBuf,
sync::{Arc, Mutex},
};

use iced::{Command, Subscription};
use liana_ui::{component::modal::Modal, widget::Element};
use tokio::task::JoinHandle;

use crate::{
app::{
message::Message,
view::{self, export::export_modal},
},
daemon::Daemon,
export::{self, get_path, ExportMessage, ExportProgress, ExportState},
};

#[derive(Debug)]
pub struct ExportModal {
path: Option<PathBuf>,
handle: Option<Arc<Mutex<JoinHandle<()>>>>,
state: ExportState,
error: Option<export::Error>,
daemon: Arc<dyn Daemon + Sync + Send>,
}

impl ExportModal {
#[allow(clippy::new_without_default)]
pub fn new(daemon: Arc<dyn Daemon + Sync + Send>) -> Self {
Self {
path: None,
handle: None,
state: ExportState::Init,
error: None,
daemon,
}
}

pub fn launch(&self) -> Command<Message> {
Command::perform(get_path(), |m| {
Message::View(view::Message::Export(ExportMessage::Path(m)))
})
}

pub fn update(&mut self, message: Message) -> Command<Message> {
if let Message::View(view::Message::Export(m)) = message {
match m {
ExportMessage::ExportProgress(m) => match m {
ExportProgress::Started(handle) => {
self.handle = Some(handle);
self.state = ExportState::Progress(0.0);
}
ExportProgress::Progress(p) => {
if let ExportState::Progress(_) = self.state {
self.state = ExportState::Progress(p);
}
}
ExportProgress::Finished | ExportProgress::Ended => {
self.state = ExportState::Ended
}
ExportProgress::Error(e) => self.error = Some(e),
ExportProgress::None => {}
},
ExportMessage::TimedOut => {
self.stop(ExportState::TimedOut);
}
ExportMessage::UserStop => {
self.stop(ExportState::Aborted);
}
ExportMessage::Path(p) => {
if let Some(path) = p {
self.path = Some(path);
self.start();
} else {
return Command::perform(async {}, |_| {
Message::View(view::Message::Export(ExportMessage::Close))
});
}
}
ExportMessage::Close | ExportMessage::Open => { /* unreachable */ }
}
Command::none()
} else {
Command::none()
}
}
pub fn view<'a>(&'a self, content: Element<'a, view::Message>) -> Element<view::Message> {
let modal = Modal::new(
content,
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)),
_ => modal,
}
.into()
}

pub fn start(&mut self) {
self.state = ExportState::Started;
}

pub fn stop(&mut self, state: ExportState) {
if let Some(handle) = self.handle.take() {
handle.lock().expect("poisoned").abort();
self.state = state;
}
}

pub fn subscription(&self) -> Option<Subscription<export::ExportProgress>> {
if let Some(path) = &self.path {
match &self.state {
ExportState::Started | ExportState::Progress(_) => {
Some(iced::subscription::unfold(
"transactions",
export::State::new(self.daemon.clone(), Box::new(path.to_path_buf())),
export::export_subscription,
))
}
_ => None,
}
} else {
None
}
}
}
1 change: 1 addition & 0 deletions liana-gui/src/app/state/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod coins;
mod export;
mod label;
mod psbt;
mod psbts;
Expand Down
72 changes: 56 additions & 16 deletions liana-gui/src/app/state/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,30 @@ use crate::{
wallet::Wallet,
},
daemon::model::{self, LabelsLoader},
export::ExportMessage,
};

use crate::daemon::{
model::{CreateSpendResult, HistoryTransaction, LabelItem, Labelled},
Daemon,
};

use super::export::ExportModal;

#[derive(Debug)]
pub enum TransactionsModal {
CreateRbf(CreateRbfModal),
Export(ExportModal),
None,
}

pub struct TransactionsPanel {
wallet: Arc<Wallet>,
txs: Vec<HistoryTransaction>,
labels_edited: LabelsEdited,
selected_tx: Option<HistoryTransaction>,
warning: Option<Error>,
create_rbf_modal: Option<CreateRbfModal>,
modal: TransactionsModal,
is_last_page: bool,
processing: bool,
}
Expand All @@ -54,7 +64,7 @@ impl TransactionsPanel {
txs: Vec::new(),
labels_edited: LabelsEdited::default(),
warning: None,
create_rbf_modal: None,
modal: TransactionsModal::None,
is_last_page: false,
processing: false,
}
Expand All @@ -63,7 +73,7 @@ impl TransactionsPanel {
pub fn preselect(&mut self, tx: HistoryTransaction) {
self.selected_tx = Some(tx);
self.warning = None;
self.create_rbf_modal = None;
self.modal = TransactionsModal::None;
}
}

Expand All @@ -76,19 +86,22 @@ impl State for TransactionsPanel {
self.labels_edited.cache(),
self.warning.as_ref(),
);
if let Some(modal) = &self.create_rbf_modal {
modal.view(content)
} else {
content
match &self.modal {
TransactionsModal::CreateRbf(rbf) => rbf.view(content),
_ => content,
}
} else {
view::transactions::transactions_view(
let content = view::transactions::transactions_view(
cache,
&self.txs,
self.warning.as_ref(),
self.is_last_page,
self.processing,
)
);
match &self.modal {
TransactionsModal::Export(export) => export.view(content),
_ => content,
}
}
}

Expand Down Expand Up @@ -134,7 +147,7 @@ impl State for TransactionsPanel {
Message::RbfModal(tx, is_cancel, res) => match res {
Ok(descendant_txids) => {
let modal = CreateRbfModal::new(*tx, is_cancel, descendant_txids);
self.create_rbf_modal = Some(modal);
self.modal = TransactionsModal::CreateRbf(modal);
}
Err(e) => {
self.warning = e.into();
Expand All @@ -146,16 +159,16 @@ impl State for TransactionsPanel {
Message::View(view::Message::Select(i)) => {
self.selected_tx = self.txs.get(i).cloned();
// Clear modal if it's for a different tx.
if let Some(modal) = &self.create_rbf_modal {
if let TransactionsModal::CreateRbf(modal) = &self.modal {
if Some(modal.tx.tx.txid())
!= self.selected_tx.as_ref().map(|selected| selected.tx.txid())
{
self.create_rbf_modal = None;
self.modal = TransactionsModal::None;
}
}
}
Message::View(view::Message::CreateRbf(view::CreateRbfMessage::Cancel)) => {
self.create_rbf_modal = None;
self.modal = TransactionsModal::None;
}
Message::View(view::Message::CreateRbf(view::CreateRbfMessage::New(is_cancel))) => {
if let Some(tx) = &self.selected_tx {
Expand Down Expand Up @@ -249,11 +262,26 @@ impl State for TransactionsPanel {
);
}
}
_ => {
if let Some(modal) = &mut self.create_rbf_modal {
return modal.update(daemon, _cache, message);
Message::View(view::Message::Export(ExportMessage::Open)) => {
if let TransactionsModal::None = &self.modal {
self.modal = TransactionsModal::Export(ExportModal::new(daemon));
if let TransactionsModal::Export(m) = &self.modal {
return m.launch();
}
}
}
Message::View(view::Message::Export(ExportMessage::Close)) => {
if let TransactionsModal::Export(_) = &self.modal {
self.modal = TransactionsModal::None;
}
}
_ => {
return match &mut self.modal {
TransactionsModal::CreateRbf(modal) => modal.update(daemon, _cache, message),
TransactionsModal::Export(modal) => modal.update(message),
TransactionsModal::None => Command::none(),
};
}
};
Command::none()
}
Expand Down Expand Up @@ -284,6 +312,17 @@ impl State for TransactionsPanel {
Message::HistoryTransactions,
)])
}

fn subscription(&self) -> iced::Subscription<Message> {
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)))
});
}
}
iced::Subscription::none()
}
}

impl From<TransactionsPanel> for Box<dyn State> {
Expand All @@ -292,6 +331,7 @@ impl From<TransactionsPanel> for Box<dyn State> {
}
}

#[derive(Debug)]
pub struct CreateRbfModal {
/// Transaction to replace.
tx: model::HistoryTransaction,
Expand Down
Loading

0 comments on commit 22f4875

Please sign in to comment.