Merge #26: Add coins panel

b849f9bb3d8577f6a0c52e6903842ab72d602583 Update minisafe master (edouard)
86eddb28c625a96a0e9b3db7cc80dbb3eb0813f5 add coins panel to gui (edouard)

Pull request description:

  based on #22

ACKs for top commit:
  edouardparis:
    Self-ACK b849f9bb3d8577f6a0c52e6903842ab72d602583

Tree-SHA512: 9959ecd84594ce94c3c9ae383344615b533a1dc968268cb8a8336840750a9c6ee82ab29a7f45999e5d27ab79a296dd2396a0d9e56890f7063c38717e0f0f6391
This commit is contained in:
edouard 2022-10-19 13:36:58 +02:00
commit 382962a47b
No known key found for this signature in database
GPG Key ID: E65F7A089C20DC8F
13 changed files with 232 additions and 17 deletions

3
gui/Cargo.lock generated
View File

@ -1390,9 +1390,10 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "minisafe"
version = "0.0.1"
source = "git+https://github.com/revault/minisafe?branch=master#4a802890634f60d77e4da5a56f9189024ef99500"
source = "git+https://github.com/revault/minisafe?branch=master#9bb20303e71cd95528b39e0aa7c8d44f3e61d1c2"
dependencies = [
"backtrace",
"base64",
"dirs",
"fern",
"jsonrpc",

View File

@ -3,4 +3,5 @@ pub enum Menu {
Home,
Receive,
Settings,
Coins,
}

View File

@ -21,7 +21,7 @@ pub use minisafe::config::Config as DaemonConfig;
pub use config::Config;
pub use message::Message;
use state::{Home, ReceivePanel, State};
use state::{CoinsPanel, Home, ReceivePanel, State};
use crate::{
app::{cache::Cache, error::Error, menu::Menu},
@ -63,6 +63,7 @@ impl App {
.into()
}
menu::Menu::Home => Home::new(&self.cache.coins).into(),
menu::Menu::Coins => CoinsPanel::new(&self.cache.coins).into(),
menu::Menu::Receive => ReceivePanel::default().into(),
};
self.state.load(self.daemon.clone())

View File

@ -0,0 +1,79 @@
use std::sync::Arc;
use iced::pure::Element;
use iced::Command;
use crate::{
app::{cache::Cache, error::Error, menu::Menu, message::Message, state::State, view},
daemon::{model::Coin, Daemon},
};
pub struct CoinsPanel {
coins: Vec<Coin>,
selected_coin: Option<usize>,
warning: Option<Error>,
}
impl CoinsPanel {
pub fn new(coins: &[Coin]) -> Self {
Self {
coins: coins.to_owned(),
selected_coin: None,
warning: None,
}
}
}
impl State for CoinsPanel {
fn view<'a>(&'a self, _cache: &'a Cache) -> Element<'a, view::Message> {
view::dashboard(
&Menu::Coins,
self.warning.as_ref(),
view::coins::coins_view(&self.coins),
)
}
fn update(
&mut self,
_daemon: Arc<dyn Daemon + Sync + Send>,
_cache: &Cache,
message: Message,
) -> Command<Message> {
match message {
Message::Coins(res) => match res {
Err(e) => self.warning = Some(e),
Ok(coins) => {
self.warning = None;
self.coins = coins;
}
},
Message::View(view::Message::Close) => {
self.selected_coin = None;
}
Message::View(view::Message::Select(i)) => {
self.selected_coin = Some(i);
}
_ => {}
};
Command::none()
}
fn load(&self, daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
let daemon = daemon.clone();
Command::perform(
async move {
daemon
.list_coins()
.map(|res| res.coins)
.map_err(|e| e.into())
},
Message::Coins,
)
}
}
impl From<CoinsPanel> for Box<dyn State> {
fn from(s: CoinsPanel) -> Box<dyn State> {
Box::new(s)
}
}

View File

@ -1,3 +1,4 @@
mod coins;
mod settings;
use std::sync::Arc;
@ -8,6 +9,8 @@ use iced::{widget::qr_code, Command, Subscription};
use super::{cache::Cache, error::Error, menu::Menu, message::Message, view};
use crate::daemon::{model::Coin, Daemon};
pub use coins::CoinsPanel;
pub use settings::SettingsState;
pub trait State {

61
gui/src/app/view/coins.rs Normal file
View File

@ -0,0 +1,61 @@
use iced::{
pure::{button, column, container, row, Element},
Alignment, Length,
};
use crate::ui::component::{badge, button::Style, card, text::*};
use crate::{app::view::message::Message, daemon::model::Coin};
pub fn coins_view<'a>(coins: &[Coin]) -> Element<'a, Message> {
column()
.push(
container(
row()
.push(text(&format!(" {}", coins.len())).bold())
.push(text(" coins")),
)
.width(Length::Fill),
)
.push(
column().spacing(10).push(
coins
.iter()
.enumerate()
.fold(column().spacing(10), |col, (i, coin)| {
col.push(coin_list_view(i, coin))
}),
),
)
.align_items(Alignment::Center)
.spacing(20)
.into()
}
fn coin_list_view<'a>(i: usize, coin: &Coin) -> Element<'a, Message> {
container(
button(
row()
.push(
row()
.push(badge::coin())
.push(text(&format!("block: {}", coin.block_height.unwrap_or(0))).small())
.spacing(10)
.align_items(Alignment::Center)
.width(Length::Fill),
)
.push(
text(&format!("{} BTC", coin.amount.as_btc()))
.bold()
.width(Length::Shrink),
)
.align_items(Alignment::Center)
.spacing(20),
)
.padding(10)
.on_press(Message::Select(i))
.style(Style::TransparentBorder),
)
.style(card::SimpleCardStyle)
.into()
}

View File

@ -5,6 +5,8 @@ pub enum Message {
Reload,
Clipboard(String),
Menu(Menu),
Close,
Select(usize),
Settings(usize, SettingsMessage),
}

View File

@ -1,6 +1,7 @@
mod message;
mod warning;
pub mod coins;
pub mod home;
pub mod receive;
pub mod settings;
@ -16,7 +17,7 @@ use iced::{
use crate::ui::{
color,
component::{button, separation, text::*},
icon::{home_icon, receive_icon, settings_icon},
icon::{coin_icon, home_icon, receive_icon, settings_icon},
};
use crate::app::{error::Error, menu::Menu};
@ -32,6 +33,16 @@ pub fn sidebar(menu: &Menu) -> widget::Container<Message> {
.width(iced::Length::Units(200))
};
let coins_button = if *menu == Menu::Coins {
button::primary(Some(coin_icon()), "Coins")
.on_press(Message::Reload)
.width(iced::Length::Units(200))
} else {
button::transparent(Some(coin_icon()), "Coins")
.on_press(Message::Menu(Menu::Coins))
.width(iced::Length::Units(200))
};
let receive_button = if *menu == Menu::Receive {
button::primary(Some(receive_icon()), "Receive")
.on_press(Message::Reload)
@ -64,8 +75,9 @@ pub fn sidebar(menu: &Menu) -> widget::Container<Message> {
.spacing(10),
)
.push(home_button)
.push(coins_button)
.push(receive_button)
.spacing(20)
.spacing(15)
.height(Length::Fill),
)
.push(container(settings_button).height(Length::Shrink)),

View File

@ -3,7 +3,7 @@ use std::convert::TryFrom;
use bitcoin::Network;
use minisafe::{
config::{BitcoinConfig, BitcoindConfig, Config as MinisafeConfig},
miniscript::{Descriptor, DescriptorPublicKey},
descriptors::InheritanceDescriptor,
};
use serde::Serialize;
@ -14,7 +14,7 @@ use std::{net::SocketAddr, path::PathBuf, time::Duration};
#[derive(Debug, Clone, Serialize)]
pub struct Config {
#[serde(serialize_with = "serialize_option_to_string")]
pub main_descriptor: Option<Descriptor<DescriptorPublicKey>>,
pub main_descriptor: Option<InheritanceDescriptor>,
pub bitcoin_config: BitcoinConfig,
/// Everything we need to know to talk to bitcoind
pub bitcoind_config: BitcoindConfig,

View File

@ -3,7 +3,7 @@ use std::str::FromStr;
use iced::pure::Element;
use minisafe::{
descriptors::inheritance_descriptor,
descriptors::InheritanceDescriptor,
miniscript::descriptor::{Descriptor, DescriptorPublicKey},
};
@ -153,9 +153,7 @@ impl Step for DefineDescriptor {
}
false
} else if !self.imported_descriptor.value.is_empty() {
if let Ok(desc) =
Descriptor::<DescriptorPublicKey>::from_str(&self.imported_descriptor.value)
{
if let Ok(desc) = InheritanceDescriptor::from_str(&self.imported_descriptor.value) {
config.main_descriptor = Some(desc);
true
} else {
@ -176,7 +174,11 @@ impl Step for DefineDescriptor {
return false;
}
match inheritance_descriptor(user_key.unwrap(), heir_key.unwrap(), sequence.unwrap()) {
match InheritanceDescriptor::new(
user_key.unwrap(),
heir_key.unwrap(),
sequence.unwrap(),
) {
Ok(desc) => {
config.main_descriptor = Some(desc);
true

View File

@ -3,7 +3,7 @@ use iced::{
Length,
};
use crate::ui::color;
use crate::ui::{color, icon};
pub enum Style {
Standard,
@ -68,3 +68,52 @@ impl<'a, Message: 'a, S: 'a + widget::container::StyleSheet> From<Badge<S>>
.into()
}
}
pub struct ReceiveStyle;
impl widget::container::StyleSheet for ReceiveStyle {
fn style(&self) -> widget::container::Style {
widget::container::Style {
border_radius: 40.0,
background: color::BACKGROUND.into(),
..widget::container::Style::default()
}
}
}
pub fn receive<T>() -> widget::container::Container<'static, T> {
container(icon::receive_icon().width(Length::Units(20)))
.width(Length::Units(40))
.height(Length::Units(40))
.style(ReceiveStyle)
.center_x()
.center_y()
}
pub struct SpendStyle;
impl widget::container::StyleSheet for SpendStyle {
fn style(&self) -> widget::container::Style {
widget::container::Style {
border_radius: 40.0,
background: color::BACKGROUND.into(),
..widget::container::Style::default()
}
}
}
pub fn spend<T>() -> widget::container::Container<'static, T> {
container(icon::send_icon().width(Length::Units(20)))
.width(Length::Units(40))
.height(Length::Units(40))
.style(ReceiveStyle)
.center_x()
.center_y()
}
pub fn coin<T>() -> widget::container::Container<'static, T> {
container(icon::coin_icon().width(Length::Units(20)))
.width(Length::Units(40))
.height(Length::Units(40))
.style(ReceiveStyle)
.center_x()
.center_y()
}

View File

@ -60,7 +60,7 @@ impl button::StyleSheet for Style {
border_radius: 10.0,
border_width: 0.0,
border_color: Color::TRANSPARENT,
text_color: color::DARK_GREY,
text_color: Color::BLACK,
},
}
}
@ -77,18 +77,18 @@ impl button::StyleSheet for Style {
},
Style::Transparent => button::Style {
shadow_offset: Vector::default(),
background: color::FOREGROUND.into(),
background: color::BACKGROUND.into(),
border_radius: 10.0,
border_width: 0.0,
border_color: Color::TRANSPARENT,
text_color: color::DARK_GREY,
text_color: Color::BLACK,
},
Style::TransparentBorder => button::Style {
shadow_offset: Vector::default(),
background: Color::TRANSPARENT.into(),
border_radius: 10.0,
border_width: 2.0,
border_color: Color::TRANSPARENT,
border_width: 1.0,
border_color: Color::BLACK,
text_color: Color::BLACK,
},
}

View File

@ -66,6 +66,10 @@ pub fn vaults_icon() -> Text {
icon('\u{F1C7}')
}
pub fn coin_icon() -> Text {
icon('\u{F567}')
}
pub fn settings_icon() -> Text {
icon('\u{F3E5}')
}