From 445ad733fbb6604f2b1acfc93db454eb4794baf7 Mon Sep 17 00:00:00 2001 From: edouard Date: Mon, 30 Jan 2023 16:40:36 +0100 Subject: [PATCH] Add signatures information to spend --- gui/src/app/state/spend/detail.rs | 27 ++- gui/src/app/view/spend/detail.rs | 290 +++++++++++++++++++++++------- gui/src/daemon/model.rs | 13 +- gui/src/ui/icon.rs | 4 + 4 files changed, 263 insertions(+), 71 deletions(-) diff --git a/gui/src/app/state/spend/detail.rs b/gui/src/app/state/spend/detail.rs index 6dd2f21c..81479859 100644 --- a/gui/src/app/state/spend/detail.rs +++ b/gui/src/app/state/spend/detail.rs @@ -1,9 +1,12 @@ use std::sync::Arc; use iced::{Command, Element}; -use liana::miniscript::bitcoin::{ - consensus, - util::{bip32::Fingerprint, psbt::Psbt}, +use liana::{ + descriptors::LianaDescInfo, + miniscript::bitcoin::{ + consensus, + util::{bip32::Fingerprint, psbt::Psbt}, + }, }; use crate::{ @@ -39,6 +42,7 @@ trait Action { pub struct SpendTxState { wallet: Arc, + desc_info: LianaDescInfo, tx: SpendTx, saved: bool, action: Option>, @@ -47,6 +51,7 @@ pub struct SpendTxState { impl SpendTxState { pub fn new(wallet: Arc, tx: SpendTx, saved: bool) -> Self { Self { + desc_info: wallet.main_descriptor.info(), wallet, action: None, tx, @@ -116,7 +121,13 @@ impl SpendTxState { } pub fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> { - let content = detail::spend_view(&self.tx, self.saved, cache.network); + let content = detail::spend_view( + &self.tx, + self.saved, + &self.desc_info, + &self.wallet.keys_aliases, + cache.network, + ); if let Some(action) = &self.action { modal::Modal::new(content, action.view()) .on_blur(Some(view::Message::Spend(view::SpendTxMessage::Cancel))) @@ -311,7 +322,10 @@ impl Action for SignAction { } }, Message::Updated(res) => match res { - Ok(()) => self.processing = false, + Ok(()) => { + self.processing = false; + tx.sigs = wallet.main_descriptor.partial_spend_info(&tx.psbt).unwrap(); + } Err(e) => self.error = Some(e), }, // We add the new hws without dropping the reference of the previous ones. @@ -393,7 +407,7 @@ impl Action for UpdateAction { fn update( &mut self, - _wallet: &Wallet, + wallet: &Wallet, daemon: Arc, message: Message, tx: &mut SpendTx, @@ -430,6 +444,7 @@ impl Action for UpdateAction { .extend(updated_input.partial_sigs.clone().into_iter()); } } + tx.sigs = wallet.main_descriptor.partial_spend_info(&tx.psbt).unwrap(); } Err(e) => self.error = e.into(), } diff --git a/gui/src/app/view/spend/detail.rs b/gui/src/app/view/spend/detail.rs index d3f0c708..a1a8267c 100644 --- a/gui/src/app/view/spend/detail.rs +++ b/gui/src/app/view/spend/detail.rs @@ -1,9 +1,14 @@ +use std::collections::HashMap; + use iced::{ - widget::{Button, Column, Container, Row, Scrollable, Space}, + widget::{scrollable, tooltip, Button, Column, Container, Row, Scrollable, Space}, Alignment, Element, Length, }; -use liana::miniscript::bitcoin::{util::bip32::Fingerprint, Address, Amount, Network, Transaction}; +use liana::{ + descriptors::{LianaDescInfo, PathInfo, PathSpendInfo}, + miniscript::bitcoin::{util::bip32::Fingerprint, Address, Amount, Network, Transaction}, +}; use crate::{ app::{ @@ -25,7 +30,13 @@ use crate::{ }, }; -pub fn spend_view(tx: &SpendTx, saved: bool, network: Network) -> Element { +pub fn spend_view<'a>( + tx: &'a SpendTx, + saved: bool, + desc_info: &'a LianaDescInfo, + key_aliases: &'a HashMap, + network: Network, +) -> Element<'a, Message> { spend_modal( saved, None, @@ -33,7 +44,7 @@ pub fn spend_view(tx: &SpendTx, saved: bool, network: Network) -> Element(tx: &SpendTx) -> Element<'a, Message> { .push( Row::new() .push(badge::Badge::new(icon::send_icon()).style(badge::Style::Standard)) - .push(text("Spend").bold()) + .push(if tx.sigs.recovery_path().is_some() { + text("Recovery").bold() + } else { + text("Spend").bold() + }) .spacing(5) .align_items(Alignment::Center), ) @@ -217,67 +232,17 @@ fn spend_header<'a>(tx: &SpendTx) -> Element<'a, Message> { .into() } -fn spend_overview_view<'a>(tx: &SpendTx) -> Element<'a, Message> { - card::simple( +fn spend_overview_view<'a>( + tx: &'a SpendTx, + desc_info: &'a LianaDescInfo, + key_aliases: &'a HashMap, +) -> Element<'a, Message> { + Container::new( Column::new() - .push(Container::new( - Row::new() - .push( - Container::new( - Row::new() - .push(Container::new( - icon::key_icon().size(30).width(Length::Fill), - )) - .push( - Column::new() - .push(text("Number of signatures:").bold()) - .push(text(format!( - "{}", - tx.psbt.inputs[0].partial_sigs.len(), - ))) - .width(Length::Fill), - ) - .push_maybe(if tx.status == SpendStatus::Pending { - if !tx.is_signed() { - Some( - button::primary(None, "Sign") - .on_press(Message::Spend(SpendTxMessage::Sign)), - ) - } else { - Some( - button::primary(None, "Broadcast").on_press( - Message::Spend(SpendTxMessage::Broadcast), - ), - ) - } - } else { - None - }) - .align_items(Alignment::Center) - .spacing(20), - ) - .width(Length::FillPortion(1)), - ) - .align_items(Alignment::Center) - .spacing(20), - )) - .push(separation().width(Length::Fill)) .push( Column::new() + .padding(15) .spacing(10) - .push( - Row::new() - .push(text("Tx ID:").bold().width(Length::Fill)) - .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() .align_items(Alignment::Center) @@ -295,10 +260,209 @@ fn spend_overview_view<'a>(tx: &SpendTx) -> Element<'a, Message> { ), ) .align_items(Alignment::Center), + ) + .push( + Row::new() + .push(text("Tx ID:").bold().width(Length::Fill)) + .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), ), ) - .spacing(20), + .push(signatures(tx, desc_info, key_aliases)), ) + .style(card::SimpleCardStyle) + .into() +} + +pub fn signatures<'a>( + tx: &'a SpendTx, + desc_info: &'a LianaDescInfo, + keys_aliases: &'a HashMap, +) -> Element<'a, Message> { + Column::new() + .push(Collapse::new( + move || { + Button::new( + Row::new() + .align_items(Alignment::Center) + .push(if tx.is_ready() { + Row::new() + .spacing(5) + .align_items(Alignment::Center) + .push(icon::circle_check_icon().style(color::SUCCESS)) + .push(text("Ready").bold().style(color::SUCCESS)) + .width(Length::Fill) + } else { + Row::new() + .spacing(5) + .align_items(Alignment::Center) + .push(icon::circle_cross_icon()) + .push(text("Not ready").bold()) + .width(Length::Fill) + }) + .push(icon::collapse_icon()), + ) + .padding(15) + .width(Length::Fill) + .style(button::Style::TransparentBorder.into()) + }, + move || { + Button::new( + Row::new() + .align_items(Alignment::Center) + .push(if tx.is_ready() { + Row::new() + .spacing(5) + .align_items(Alignment::Center) + .push(icon::circle_check_icon().style(color::SUCCESS)) + .push(text("Ready").bold().style(color::SUCCESS)) + .width(Length::Fill) + } else { + Row::new() + .spacing(5) + .align_items(Alignment::Center) + .push(icon::circle_cross_icon()) + .push(text("Not ready").bold()) + .width(Length::Fill) + }) + .push(icon::collapsed_icon()), + ) + .padding(15) + .width(Length::Fill) + .style(button::Style::TransparentBorder.into()) + }, + move || { + Into::>::into( + Column::new().push(separation().width(Length::Fill)).push( + Column::new() + .padding(15) + .spacing(10) + .push(path_view( + desc_info.primary_path(), + tx.sigs.primary_path(), + keys_aliases, + )) + .push_maybe(tx.sigs.recovery_path().as_ref().map(|path| { + let (_, keys) = desc_info.recovery_path(); + path_view(keys, path, keys_aliases) + })), + ), + ) + }, + )) + .push_maybe(if tx.status == SpendStatus::Pending { + Some( + Column::new().push(separation().width(Length::Fill)).push( + Container::new( + Row::new() + .push(Space::with_width(Length::Fill)) + .push_maybe(if !tx.is_ready() { + Some( + button::primary(None, "Sign") + .on_press(Message::Spend(SpendTxMessage::Sign)) + .width(Length::Units(150)), + ) + } else { + Some( + button::primary(None, "Broadcast") + .on_press(Message::Spend(SpendTxMessage::Broadcast)) + .width(Length::Units(150)), + ) + }) + .align_items(Alignment::Center) + .spacing(20), + ) + .padding(15), + ), + ) + } else { + None + }) + .into() +} + +pub fn path_view<'a>( + path: &'a PathInfo, + sigs: &'a PathSpendInfo, + key_aliases: &'a HashMap, +) -> Element<'a, Message> { + let mut keys: Vec = path.thresh_fingerprints().1.into_iter().collect(); + keys.sort(); + Scrollable::new( + Row::new() + .spacing(5) + .align_items(Alignment::Center) + .push(if sigs.signed_pubkeys.len() >= sigs.threshold { + icon::circle_check_icon().style(color::SUCCESS) + } else { + icon::circle_cross_icon() + }) + .push( + Container::new(text(format!(" {} ", sigs.threshold))).style( + if sigs.signed_pubkeys.len() >= sigs.threshold { + badge::PillStyle::Success + } else { + badge::PillStyle::Simple + }, + ), + ) + .push(text(format!( + "signature{} out of", + if sigs.threshold > 1 { "s" } else { "" } + ))) + .push( + sigs.signed_pubkeys + .keys() + .fold(Row::new().spacing(5), |row, value| { + row.push(if let Some(alias) = key_aliases.get(value) { + Container::new( + tooltip::Tooltip::new( + Container::new(text(alias)) + .padding(3) + .style(badge::PillStyle::Success), + value.to_string(), + tooltip::Position::Bottom, + ) + .style(card::SimpleCardStyle), + ) + } else { + Container::new(text(value.to_string())) + .padding(3) + .style(badge::PillStyle::Success) + }) + }), + ) + .push(keys.iter().fold(Row::new().spacing(5), |row, &value| { + row.push_maybe(if !sigs.signed_pubkeys.contains_key(&value) { + Some(if let Some(alias) = key_aliases.get(&value) { + Container::new( + tooltip::Tooltip::new( + Container::new(text(alias)) + .padding(3) + .style(badge::PillStyle::Simple), + value.to_string(), + tooltip::Position::Bottom, + ) + .style(card::SimpleCardStyle), + ) + } else { + Container::new(text(value.to_string())) + .padding(3) + .style(badge::PillStyle::Simple) + }) + } else { + None + }) + })), + ) + .horizontal_scroll(scrollable::Properties::new().width(2).scroller_width(2)) .into() } diff --git a/gui/src/daemon/model.rs b/gui/src/daemon/model.rs index 10958940..9f38582f 100644 --- a/gui/src/daemon/model.rs +++ b/gui/src/daemon/model.rs @@ -78,8 +78,17 @@ impl SpendTx { } } - pub fn is_signed(&self) -> bool { - !self.psbt.inputs.first().unwrap().partial_sigs.is_empty() + pub fn is_ready(&self) -> bool { + let path = self.sigs.primary_path(); + if path.signed_pubkeys.len() >= path.threshold { + return true; + } + if let Some(path) = self.sigs.recovery_path() { + if path.signed_pubkeys.len() >= path.threshold { + return true; + } + } + false } } diff --git a/gui/src/ui/icon.rs b/gui/src/ui/icon.rs index 92141dc4..ec082572 100644 --- a/gui/src/ui/icon.rs +++ b/gui/src/ui/icon.rs @@ -121,6 +121,10 @@ pub fn circle_check_icon() -> Text<'static> { icon('\u{F26B}') } +pub fn circle_cross_icon() -> Text<'static> { + icon('\u{F623}') +} + pub fn network_icon() -> Text<'static> { icon('\u{F40D}') }