From c4ccbf011399d0425cfb7b54da6d3b0c3f4ef385 Mon Sep 17 00:00:00 2001 From: edouard Date: Wed, 4 Jan 2023 17:36:27 +0100 Subject: [PATCH] gui: import a spend draft --- gui/src/app/state/spend/detail.rs | 2 +- gui/src/app/state/spend/mod.rs | 108 +++++++++++++++++++++++++++++- gui/src/app/view/message.rs | 8 +++ gui/src/app/view/spend/mod.rs | 71 +++++++++++++++++--- gui/src/ui/component/modal.rs | 7 +- 5 files changed, 179 insertions(+), 17 deletions(-) diff --git a/gui/src/app/state/spend/detail.rs b/gui/src/app/state/spend/detail.rs index 49567f85..78be7014 100644 --- a/gui/src/app/state/spend/detail.rs +++ b/gui/src/app/state/spend/detail.rs @@ -110,7 +110,7 @@ impl SpendTxState { let content = detail::spend_view(&self.tx, self.saved, cache.network); if let Some(action) = &self.action { modal::Modal::new(content, action.view()) - .on_blur(view::Message::Spend(view::SpendTxMessage::Cancel)) + .on_blur(Some(view::Message::Spend(view::SpendTxMessage::Cancel))) .into() } else { content diff --git a/gui/src/app/state/spend/mod.rs b/gui/src/app/state/spend/mod.rs index cbbe990f..aa0ce98f 100644 --- a/gui/src/app/state/spend/mod.rs +++ b/gui/src/app/state/spend/mod.rs @@ -4,7 +4,10 @@ use std::sync::Arc; use iced::{Command, Element}; -use liana::descriptors::MultipathDescriptor; +use liana::{ + descriptors::MultipathDescriptor, + miniscript::bitcoin::{consensus, util::psbt::Psbt}, +}; use super::{redirect, State}; use crate::{ @@ -13,6 +16,7 @@ use crate::{ model::{Coin, SpendTx}, Daemon, }, + ui::component::{form, modal}, }; pub struct SpendPanel { @@ -20,6 +24,7 @@ pub struct SpendPanel { selected_tx: Option, spend_txs: Vec, warning: Option, + import_tx: Option, } impl SpendPanel { @@ -29,6 +34,7 @@ impl SpendPanel { spend_txs: spend_txs.to_vec(), warning: None, selected_tx: None, + import_tx: None, } } } @@ -38,12 +44,23 @@ impl State for SpendPanel { if let Some(tx) = &self.selected_tx { tx.view(cache) } else { - view::dashboard( + let list_view = view::dashboard( &Menu::Spend, cache, self.warning.as_ref(), view::spend::spend_view(&self.spend_txs), - ) + ); + if let Some(import_tx) = &self.import_tx { + modal::Modal::new(list_view, import_tx.view()) + .on_blur(if import_tx.processing { + None + } else { + Some(view::Message::Close) + }) + .into() + } else { + list_view + } } } @@ -61,11 +78,20 @@ impl State for SpendPanel { self.spend_txs = txs; } }, + Message::View(view::Message::ImportSpend(view::ImportSpendMessage::Import)) => { + if self.import_tx.is_none() { + self.import_tx = Some(ImportSpendState::new()); + } + } Message::View(view::Message::Close) => { if self.selected_tx.is_some() { self.selected_tx = None; return self.load(daemon); } + if self.import_tx.is_some() { + self.import_tx = None; + return self.load(daemon); + } } Message::View(view::Message::Select(i)) => { if let Some(tx) = self.spend_txs.get(i) { @@ -79,6 +105,10 @@ impl State for SpendPanel { if let Some(tx) = &mut self.selected_tx { return tx.update(daemon, cache, message); } + + if let Some(import_tx) = &mut self.import_tx { + return import_tx.update(daemon, cache, message); + } } } Command::none() @@ -188,3 +218,75 @@ impl From for Box { Box::new(s) } } + +pub struct ImportSpendState { + imported: form::Value, + processing: bool, + error: Option, + success: bool, +} + +impl ImportSpendState { + pub fn new() -> Self { + Self { + imported: form::Value::default(), + processing: false, + error: None, + success: false, + } + } +} + +impl ImportSpendState { + fn view<'a>(&self) -> Element<'a, view::Message> { + if self.success { + view::spend::import_spend_success_view() + } else { + view::spend::import_spend_view(&self.imported, self.error.as_ref(), self.processing) + } + } + + fn update( + &mut self, + daemon: Arc, + _cache: &Cache, + message: Message, + ) -> Command { + match message { + Message::Updated(res) => { + self.processing = false; + match res { + Ok(()) => { + self.success = true; + self.error = None; + } + Err(e) => self.error = e.into(), + } + } + Message::View(view::Message::ImportSpend(view::ImportSpendMessage::PsbtEdited(s))) => { + self.imported.value = s; + self.imported.valid = base64::decode(&self.imported.value) + .ok() + .and_then(|bytes| consensus::encode::deserialize::(&bytes).ok()) + .is_some(); + } + Message::View(view::Message::ImportSpend(view::ImportSpendMessage::Confirm)) => { + if self.imported.valid { + self.processing = true; + self.error = None; + let imported: Psbt = consensus::encode::deserialize( + &base64::decode(&self.imported.value).expect("Already checked"), + ) + .unwrap(); + return Command::perform( + async move { daemon.update_spend_tx(&imported).map_err(|e| e.into()) }, + Message::Updated, + ); + } + } + _ => {} + } + + Command::none() + } +} diff --git a/gui/src/app/view/message.rs b/gui/src/app/view/message.rs index 8cf2bc0e..c074d568 100644 --- a/gui/src/app/view/message.rs +++ b/gui/src/app/view/message.rs @@ -9,6 +9,7 @@ pub enum Message { Select(usize), Settings(usize, SettingsMessage), CreateSpend(CreateSpendMessage), + ImportSpend(ImportSpendMessage), Spend(SpendTxMessage), Next, Previous, @@ -24,6 +25,13 @@ pub enum CreateSpendMessage { Generate, } +#[derive(Debug, Clone)] +pub enum ImportSpendMessage { + Import, + PsbtEdited(String), + Confirm, +} + #[derive(Debug, Clone)] pub enum SpendTxMessage { Delete, diff --git a/gui/src/app/view/spend/mod.rs b/gui/src/app/view/spend/mod.rs index 49fdea25..1c107f31 100644 --- a/gui/src/app/view/spend/mod.rs +++ b/gui/src/app/view/spend/mod.rs @@ -2,29 +2,84 @@ pub mod detail; pub mod step; use iced::{ - widget::{Button, Column, Container, Row}, + widget::{Button, Column, Container, Row, Space}, Alignment, Element, Length, }; use crate::{ - app::menu::Menu, + app::{error::Error, menu::Menu}, daemon::model::{SpendStatus, SpendTx}, ui::{ - component::{badge, button, card, text::*}, + color, + component::{badge, button, card, form, text::*}, icon, util::Collection, }, }; -use super::message::Message; +use super::{message::*, warning::warn}; + +pub fn import_spend_view<'a>( + imported: &form::Value, + error: Option<&Error>, + processing: bool, +) -> Element<'a, Message> { + Column::new() + .push(warn(error)) + .push(card::simple( + Column::new() + .spacing(10) + .push(text("Insert PSBT:").bold()) + .push( + form::Form::new("PSBT", imported, move |msg| { + Message::ImportSpend(ImportSpendMessage::PsbtEdited(msg)) + }) + .warning("Please enter a base64 encoded PSBT") + .size(20) + .padding(10), + ) + .push(Row::new().push(Space::with_width(Length::Fill)).push( + if imported.valid && !imported.value.is_empty() && !processing { + button::primary(None, "Import") + .on_press(Message::ImportSpend(ImportSpendMessage::Confirm)) + } else if processing { + button::primary(None, "Processing...") + } else { + button::primary(None, "Import") + }, + )), + )) + .max_width(400) + .into() +} + +pub fn import_spend_success_view<'a>() -> Element<'a, Message> { + Column::new() + .push( + card::simple(Container::new( + text("PSBT is imported").style(color::SUCCESS), + )) + .padding(50), + ) + .width(Length::Units(400)) + .align_items(Alignment::Center) + .into() +} pub fn spend_view<'a>(spend_txs: &[SpendTx]) -> Element<'a, Message> { Column::new() .push( - Row::new().push(Column::new().width(Length::Fill)).push( - button::primary(Some(icon::plus_icon()), "New transaction") - .on_press(Message::Menu(Menu::CreateSpendTx)), - ), + Row::new() + .spacing(10) + .push(Column::new().width(Length::Fill)) + .push( + button::border(Some(icon::import_icon()), "Import") + .on_press(Message::ImportSpend(ImportSpendMessage::Import)), + ) + .push( + button::primary(Some(icon::plus_icon()), "New") + .on_press(Message::Menu(Menu::CreateSpendTx)), + ), ) .push( Container::new( diff --git a/gui/src/ui/component/modal.rs b/gui/src/ui/component/modal.rs index 13716482..b59f2538 100644 --- a/gui/src/ui/component/modal.rs +++ b/gui/src/ui/component/modal.rs @@ -28,11 +28,8 @@ impl<'a, Message, Renderer> Modal<'a, Message, Renderer> { /// Sets the message that will be produces when the background /// of the [`Modal`] is pressed - pub fn on_blur(self, on_blur: Message) -> Self { - Self { - on_blur: Some(on_blur), - ..self - } + pub fn on_blur(self, on_blur: Option) -> Self { + Self { on_blur, ..self } } }