Merge #276: gui: import and update a spend draft
9921bcadd56a0ce5ad6062730ed9a3eea4dce67a Add update psbt action to spend tx panel (edouard)
c4ccbf011399d0425cfb7b54da6d3b0c3f4ef385 gui: import a spend draft (edouard)
Pull request description:
ACKs for top commit:
edouardparis:
Self-ACK 9921bcadd56a0ce5ad6062730ed9a3eea4dce67a
Tree-SHA512: 11acdc097aafef1ad6b4bc824afbf6d1000b000b48596e60642152ec53fc03927585d24a01e78496cde6e2b356ba75f32d7b796c3d48e87b20db1629660d0839
This commit is contained in:
commit
80fb0ecb01
@ -1,7 +1,10 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use iced::{Command, Element};
|
||||
use liana::miniscript::bitcoin::util::{bip32::Fingerprint, psbt::Psbt};
|
||||
use liana::miniscript::bitcoin::{
|
||||
consensus,
|
||||
util::{bip32::Fingerprint, psbt::Psbt},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
@ -12,7 +15,7 @@ use crate::{
|
||||
Daemon,
|
||||
},
|
||||
hw::{list_hardware_wallets, HardwareWallet},
|
||||
ui::component::modal,
|
||||
ui::component::{form, modal},
|
||||
};
|
||||
|
||||
trait Action {
|
||||
@ -79,6 +82,12 @@ impl SpendTxState {
|
||||
self.action = Some(Box::new(action));
|
||||
return cmd;
|
||||
}
|
||||
view::SpendTxMessage::EditPsbt => {
|
||||
let action = UpdateAction::new(self.tx.psbt.to_string());
|
||||
let cmd = action.load(daemon);
|
||||
self.action = Some(Box::new(action));
|
||||
return cmd;
|
||||
}
|
||||
view::SpendTxMessage::Broadcast => {
|
||||
self.action = Some(Box::new(BroadcastAction::default()));
|
||||
}
|
||||
@ -110,7 +119,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
|
||||
@ -349,3 +358,110 @@ async fn sign_psbt(
|
||||
hw.sign_tx(&mut psbt).await.map_err(Error::from)?;
|
||||
Ok((psbt, fingerprint))
|
||||
}
|
||||
|
||||
pub struct UpdateAction {
|
||||
psbt: String,
|
||||
updated: form::Value<String>,
|
||||
processing: bool,
|
||||
error: Option<Error>,
|
||||
success: bool,
|
||||
}
|
||||
|
||||
impl UpdateAction {
|
||||
pub fn new(psbt: String) -> Self {
|
||||
Self {
|
||||
psbt,
|
||||
updated: form::Value::default(),
|
||||
processing: false,
|
||||
error: None,
|
||||
success: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Action for UpdateAction {
|
||||
fn view(&self) -> Element<view::Message> {
|
||||
if self.success {
|
||||
view::spend::detail::update_spend_success_view()
|
||||
} else {
|
||||
view::spend::detail::update_spend_view(
|
||||
self.psbt.clone(),
|
||||
&self.updated,
|
||||
self.error.as_ref(),
|
||||
self.processing,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
_cache: &Cache,
|
||||
message: Message,
|
||||
tx: &mut SpendTx,
|
||||
) -> Command<Message> {
|
||||
match message {
|
||||
Message::Updated(res) => {
|
||||
self.processing = false;
|
||||
match res {
|
||||
Ok(()) => {
|
||||
self.success = true;
|
||||
self.error = None;
|
||||
let psbt = consensus::encode::deserialize::<Psbt>(
|
||||
&base64::decode(&self.updated.value).unwrap(),
|
||||
)
|
||||
.expect("Already checked");
|
||||
for (i, input) in tx.psbt.inputs.iter_mut().enumerate() {
|
||||
if tx
|
||||
.psbt
|
||||
.unsigned_tx
|
||||
.input
|
||||
.get(i)
|
||||
.map(|tx_in| tx_in.previous_output)
|
||||
!= psbt
|
||||
.unsigned_tx
|
||||
.input
|
||||
.get(i)
|
||||
.map(|tx_in| tx_in.previous_output)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if let Some(updated_input) = psbt.inputs.get(i) {
|
||||
input
|
||||
.partial_sigs
|
||||
.extend(updated_input.partial_sigs.clone().into_iter());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => self.error = e.into(),
|
||||
}
|
||||
}
|
||||
Message::View(view::Message::ImportSpend(view::ImportSpendMessage::PsbtEdited(s))) => {
|
||||
self.updated.value = s;
|
||||
if let Some(psbt) = base64::decode(&self.updated.value)
|
||||
.ok()
|
||||
.and_then(|bytes| consensus::encode::deserialize::<Psbt>(&bytes).ok())
|
||||
{
|
||||
self.updated.valid = tx.psbt.unsigned_tx.txid() == psbt.unsigned_tx.txid();
|
||||
}
|
||||
}
|
||||
Message::View(view::Message::ImportSpend(view::ImportSpendMessage::Confirm)) => {
|
||||
if self.updated.valid {
|
||||
self.processing = true;
|
||||
self.error = None;
|
||||
let updated: Psbt = consensus::encode::deserialize(
|
||||
&base64::decode(&self.updated.value).expect("Already checked"),
|
||||
)
|
||||
.unwrap();
|
||||
return Command::perform(
|
||||
async move { daemon.update_spend_tx(&updated).map_err(|e| e.into()) },
|
||||
Message::Updated,
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
@ -33,6 +41,8 @@ pub enum SpendTxMessage {
|
||||
Confirm,
|
||||
Cancel,
|
||||
SelectHardwareWallet(usize),
|
||||
EditPsbt,
|
||||
PsbtEdited(String),
|
||||
Next,
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
use iced::{
|
||||
widget::{Button, Column, Container, Row, Scrollable},
|
||||
widget::{Button, Column, Container, Row, Scrollable, Space},
|
||||
Alignment, Element, Length,
|
||||
};
|
||||
|
||||
@ -13,10 +13,11 @@ use crate::{
|
||||
daemon::model::{Coin, SpendStatus, SpendTx},
|
||||
hw::HardwareWallet,
|
||||
ui::{
|
||||
color,
|
||||
component::{
|
||||
badge, button, card,
|
||||
collapse::Collapse,
|
||||
container, separation,
|
||||
container, form, separation,
|
||||
text::{text, Text},
|
||||
},
|
||||
icon,
|
||||
@ -266,18 +267,35 @@ fn spend_overview_view<'a>(tx: &SpendTx) -> Element<'a, Message> {
|
||||
.push(separation().width(Length::Fill))
|
||||
.push(
|
||||
Column::new()
|
||||
.spacing(10)
|
||||
.push(
|
||||
Row::new()
|
||||
.push(text("Tx ID:").bold().width(Length::Fill))
|
||||
.push(text(format!("{}", tx.psbt.unsigned_tx.txid())).small())
|
||||
.push(text(tx.psbt.unsigned_tx.txid().to_string()).small())
|
||||
.push(
|
||||
Button::new(icon::clipboard_icon())
|
||||
.on_press(Message::Clipboard(
|
||||
tx.psbt.unsigned_tx.txid().to_string(),
|
||||
))
|
||||
.style(button::Style::TransparentBorder.into()),
|
||||
)
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
.push(
|
||||
Row::new()
|
||||
.push(text("Psbt:").bold().width(Length::Fill))
|
||||
.align_items(Alignment::Center)
|
||||
.push(text("PSBT:").bold().width(Length::Fill))
|
||||
.push(
|
||||
button::transparent(Some(icon::clipboard_icon()), "Copy")
|
||||
.on_press(Message::Clipboard(tx.psbt.to_string())),
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.push(
|
||||
button::border(Some(icon::clipboard_icon()), "Copy")
|
||||
.on_press(Message::Clipboard(tx.psbt.to_string())),
|
||||
)
|
||||
.push(
|
||||
button::border(Some(icon::import_icon()), "Update")
|
||||
.on_press(Message::Spend(SpendTxMessage::EditPsbt)),
|
||||
),
|
||||
)
|
||||
.align_items(Alignment::Center),
|
||||
),
|
||||
@ -539,7 +557,7 @@ pub fn sign_action<'a>(
|
||||
Column::new()
|
||||
.push(
|
||||
Column::new()
|
||||
.spacing(20)
|
||||
.spacing(15)
|
||||
.width(Length::Fill)
|
||||
.push("Please connect a hardware wallet")
|
||||
.push(button::border(None, "Refresh").on_press(Message::Reload))
|
||||
@ -547,9 +565,72 @@ pub fn sign_action<'a>(
|
||||
)
|
||||
.width(Length::Fill)
|
||||
})
|
||||
.spacing(20)
|
||||
.width(Length::Fill)
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
.width(Length::Units(500))
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn update_spend_view<'a>(
|
||||
psbt: String,
|
||||
updated: &form::Value<String>,
|
||||
error: Option<&Error>,
|
||||
processing: bool,
|
||||
) -> Element<'a, Message> {
|
||||
Column::new()
|
||||
.push(warn(error))
|
||||
.push(card::simple(
|
||||
Column::new()
|
||||
.spacing(20)
|
||||
.push(
|
||||
Row::new()
|
||||
.push(text("PSBT:").bold().width(Length::Fill))
|
||||
.push(
|
||||
button::border(Some(icon::clipboard_icon()), "Copy")
|
||||
.on_press(Message::Clipboard(psbt)),
|
||||
)
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
.push(separation().width(Length::Fill))
|
||||
.push(
|
||||
Column::new()
|
||||
.spacing(10)
|
||||
.push(text("Insert updated PSBT:").bold())
|
||||
.push(
|
||||
form::Form::new("PSBT", updated, move |msg| {
|
||||
Message::ImportSpend(ImportSpendMessage::PsbtEdited(msg))
|
||||
})
|
||||
.warning("Please enter the correct base64 encoded PSBT")
|
||||
.size(20)
|
||||
.padding(10),
|
||||
)
|
||||
.push(Row::new().push(Space::with_width(Length::Fill)).push(
|
||||
if updated.valid && !updated.value.is_empty() && !processing {
|
||||
button::primary(None, "Update")
|
||||
.on_press(Message::ImportSpend(ImportSpendMessage::Confirm))
|
||||
} else if processing {
|
||||
button::primary(None, "Processing...")
|
||||
} else {
|
||||
button::primary(None, "Update")
|
||||
},
|
||||
)),
|
||||
),
|
||||
))
|
||||
.max_width(400)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn update_spend_success_view<'a>() -> Element<'a, Message> {
|
||||
Column::new()
|
||||
.push(
|
||||
card::simple(Container::new(
|
||||
text("Spend transaction is updated").style(color::SUCCESS),
|
||||
))
|
||||
.padding(50),
|
||||
)
|
||||
.width(Length::Units(400))
|
||||
.align_items(Alignment::Center)
|
||||
.into()
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user