gui: import a spend draft

This commit is contained in:
edouard 2023-01-04 17:36:27 +01:00
parent ae331c7aec
commit c4ccbf0113
5 changed files with 179 additions and 17 deletions

View File

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

View File

@ -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<detail::SpendTxState>,
spend_txs: Vec<SpendTx>,
warning: Option<Error>,
import_tx: Option<ImportSpendState>,
}
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<CreateSpendPanel> for Box<dyn State> {
Box::new(s)
}
}
pub struct ImportSpendState {
imported: form::Value<String>,
processing: bool,
error: Option<Error>,
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<dyn Daemon + Sync + Send>,
_cache: &Cache,
message: Message,
) -> Command<Message> {
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::<Psbt>(&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()
}
}

View File

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

View File

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

View File

@ -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<Message>) -> Self {
Self { on_blur, ..self }
}
}