gui: installer and loader
This commit is contained in:
parent
b548451292
commit
0b24104b45
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,3 +7,4 @@ regtest/
|
||||
venv/
|
||||
pytest.log
|
||||
TODO
|
||||
**/target
|
||||
|
||||
2977
gui/Cargo.lock
generated
Normal file
2977
gui/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
gui/Cargo.toml
Normal file
41
gui/Cargo.toml
Normal file
@ -0,0 +1,41 @@
|
||||
[package]
|
||||
name = "minisafe-gui"
|
||||
version = "0.0.1"
|
||||
readme = "README.md"
|
||||
description = "Minisafe GUI"
|
||||
repository = "https://github.com/revault/minisafe"
|
||||
license = "BSD-3-Clause"
|
||||
authors = ["Edouard Paris <m@edouard.paris>", "Daniela Brozzoni <danielabrozzoni@protonmail.com>"]
|
||||
edition = "2018"
|
||||
resolver = "2"
|
||||
|
||||
[[bin]]
|
||||
name = "minisafe-gui"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
bitcoin = { version = "0.27", features = ["base64", "use-serde"] }
|
||||
minisafe = { git = "https://github.com/revault/minisafe", branch = "master", default-features = false}
|
||||
backtrace = "0.3"
|
||||
|
||||
iced = { version = "0.4", default-features= false, features = ["tokio", "wgpu", "svg", "qr_code", "pure"] }
|
||||
iced_native = "0.5"
|
||||
|
||||
tokio = {version = "1.9.0", features = ["signal"]}
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
# Logging stuff
|
||||
log = "0.4"
|
||||
fern = "0.6"
|
||||
|
||||
dirs = "3.0.1"
|
||||
toml = "0.5"
|
||||
|
||||
chrono = "0.4"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
uds_windows = "0.1.5"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = {version = "1.9.0", features = ["rt", "macros"]}
|
||||
@ -0,0 +1,38 @@
|
||||
# minisafe-gui
|
||||
|
||||
Revault GUI is an user graphical interface written in rust for the
|
||||
[Minisafe daemon](https://github.com/revault/minisafe).
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `fontconfig` (On Debian/Ubuntu `apt install libfontconfig1-dev`)
|
||||
- [`pkg-config`](https://www.freedesktop.org/wiki/Software/pkg-config/) (On Debian/Ubuntu `apt install pkg-config`)
|
||||
- [`libxkbcommon`](https://xkbcommon.org/) for the dummy signer (On Debian/Ubuntu `apt install libxkbcommon-dev`)
|
||||
- Vulkan drivers (On Debian/Ubuntu `apt install mesa-vulkan-drivers libvulkan-dev`)
|
||||
- `libudev-dev` (On Debian/Ubuntu `apt install libudev-dev`)
|
||||
|
||||
We are striving to remove dependencies, especially the 3D ones.
|
||||
|
||||
## Usage
|
||||
|
||||
`minisafe-gui --datadir <datadir> --<network>`
|
||||
|
||||
The default `datadir` is the default `minisafed` `datadir` (`~/.minisafe`
|
||||
for linux) and the default `network` is the bitcoin mainnet.
|
||||
|
||||
If no argument is provided, the GUI checks in the default `datadir`
|
||||
the configuration file for the bitcoin mainnet.
|
||||
|
||||
If the provided `datadir` is empty or does not have the configuration
|
||||
file for the targeted `network`, the GUI starts with the installer mode.
|
||||
|
||||
Instead of using `--datadir` and `--<network>`, a direct path to
|
||||
the GUI configuration file can be provided with `--conf`.
|
||||
|
||||
After start up, The GUI will connect to the running minisafed.
|
||||
A command starting minisafed is launched if no connection is made.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If you encounter layout issue on `X11`, try to start the GUI with
|
||||
`WINIT_X11_SCALE_FACTOR` manually set to 1
|
||||
92
gui/src/app/config.rs
Normal file
92
gui/src/app/config.rs
Normal file
@ -0,0 +1,92 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Config {
|
||||
/// Path to minisafed configuration file.
|
||||
pub minisafed_config_path: PathBuf,
|
||||
/// log level, can be "info", "debug", "trace".
|
||||
pub log_level: Option<String>,
|
||||
/// Use iced debug feature if true.
|
||||
pub debug: Option<bool>,
|
||||
}
|
||||
|
||||
pub const DEFAULT_FILE_NAME: &str = "gui.toml";
|
||||
|
||||
impl Config {
|
||||
pub fn new(minisafed_config_path: PathBuf) -> Self {
|
||||
Self {
|
||||
minisafed_config_path,
|
||||
log_level: None,
|
||||
debug: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_file(path: &Path) -> Result<Self, ConfigError> {
|
||||
let config = std::fs::read(path)
|
||||
.map_err(|e| match e.kind() {
|
||||
std::io::ErrorKind::NotFound => ConfigError::NotFound,
|
||||
_ => ConfigError::ReadingFile(format!("Reading configuration file: {}", e)),
|
||||
})
|
||||
.and_then(|file_content| {
|
||||
toml::from_slice::<Config>(&file_content).map_err(|e| {
|
||||
ConfigError::ReadingFile(format!("Parsing configuration file: {}", e))
|
||||
})
|
||||
})?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn default_path() -> Result<PathBuf, ConfigError> {
|
||||
let mut datadir = default_datadir().map_err(|_| {
|
||||
ConfigError::Unexpected("Could not locate the default datadir directory.".to_owned())
|
||||
})?;
|
||||
datadir.push(DEFAULT_FILE_NAME);
|
||||
Ok(datadir)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
pub enum ConfigError {
|
||||
NotFound,
|
||||
ReadingFile(String),
|
||||
Unexpected(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ConfigError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NotFound => write!(f, "Config file not found"),
|
||||
Self::ReadingFile(e) => write!(f, "Error while reading file: {}", e),
|
||||
Self::Unexpected(e) => write!(f, "Unexpected error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ConfigError {}
|
||||
|
||||
// Get the absolute path to the minisafe configuration folder.
|
||||
///
|
||||
/// This a "minisafe" directory in the XDG standard configuration directory for all OSes but
|
||||
/// Linux-based ones, for which it's `~/.minisafe`.
|
||||
/// Rationale: we want to have the database, RPC socket, etc.. in the same folder as the
|
||||
/// configuration file but for Linux the XDG specify a data directory (`~/.local/share/`) different
|
||||
/// from the configuration one (`~/.config/`).
|
||||
pub fn default_datadir() -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
#[cfg(target_os = "linux")]
|
||||
let configs_dir = dirs::home_dir();
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let configs_dir = dirs::config_dir();
|
||||
|
||||
if let Some(mut path) = configs_dir {
|
||||
#[cfg(target_os = "linux")]
|
||||
path.push(".minisafe");
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
path.push("Minisafe");
|
||||
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
Err("Failed to get default data directory".into())
|
||||
}
|
||||
75
gui/src/app/context.rs
Normal file
75
gui/src/app/context.rs
Normal file
@ -0,0 +1,75 @@
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
|
||||
use minisafe::config::Config as DaemonConfig;
|
||||
|
||||
use crate::{
|
||||
app::{config, error::Error, menu::Menu},
|
||||
conversion::Converter,
|
||||
daemon::Daemon,
|
||||
};
|
||||
|
||||
/// Context is an object passing general information
|
||||
/// and service clients through the application components.
|
||||
pub struct Context {
|
||||
pub config: ConfigContext,
|
||||
pub blockheight: i32,
|
||||
pub daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
pub converter: Converter,
|
||||
pub menu: Menu,
|
||||
pub managers_threshold: usize,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn new(
|
||||
config: ConfigContext,
|
||||
daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
converter: Converter,
|
||||
menu: Menu,
|
||||
) -> Self {
|
||||
Self {
|
||||
config,
|
||||
blockheight: 0,
|
||||
daemon,
|
||||
converter,
|
||||
menu,
|
||||
managers_threshold: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn network(&self) -> bitcoin::Network {
|
||||
self.config.daemon.bitcoin_config.network
|
||||
}
|
||||
|
||||
pub fn load_daemon_config(&mut self, cfg: DaemonConfig) -> Result<(), Error> {
|
||||
loop {
|
||||
if let Some(daemon) = Arc::get_mut(&mut self.daemon) {
|
||||
daemon.load_config(cfg)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut daemon_config_file = OpenOptions::new()
|
||||
.write(true)
|
||||
.open(&self.config.gui.minisafed_config_path)
|
||||
.map_err(|e| Error::Config(e.to_string()))?;
|
||||
|
||||
let content =
|
||||
toml::to_string(&self.config.daemon).map_err(|e| Error::Config(e.to_string()))?;
|
||||
|
||||
daemon_config_file
|
||||
.write_all(content.as_bytes())
|
||||
.map_err(|e| {
|
||||
log::warn!("failed to write to file: {:?}", e);
|
||||
Error::Config(e.to_string())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ConfigContext {
|
||||
pub daemon: DaemonConfig,
|
||||
pub gui: config::Config,
|
||||
}
|
||||
52
gui/src/app/error.rs
Normal file
52
gui/src/app/error.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use crate::daemon::DaemonError;
|
||||
use minisafe::config::ConfigError;
|
||||
use std::convert::From;
|
||||
use std::io::ErrorKind;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Error {
|
||||
Config(String),
|
||||
Daemon(DaemonError),
|
||||
Unexpected(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Config(e) => write!(f, "{}", e),
|
||||
Self::Daemon(e) => match e {
|
||||
DaemonError::Unexpected(e) => write!(f, "{}", e),
|
||||
DaemonError::NoAnswer => write!(f, "Daemon did not answer"),
|
||||
DaemonError::Transport(Some(ErrorKind::ConnectionRefused), _) => {
|
||||
write!(f, "Failed to connect to daemon")
|
||||
}
|
||||
DaemonError::Transport(kind, e) => {
|
||||
if let Some(k) = kind {
|
||||
write!(f, "{} [{:?}]", e, k)
|
||||
} else {
|
||||
write!(f, "{}", e)
|
||||
}
|
||||
}
|
||||
DaemonError::Start(e) => {
|
||||
write!(f, "Failed to start daemon: {}", e)
|
||||
}
|
||||
DaemonError::Rpc(code, e) => {
|
||||
write!(f, "[{:?}] {}", code, e)
|
||||
}
|
||||
},
|
||||
Self::Unexpected(e) => write!(f, "Unexpected error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConfigError> for Error {
|
||||
fn from(error: ConfigError) -> Self {
|
||||
Error::Config(error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DaemonError> for Error {
|
||||
fn from(error: DaemonError) -> Self {
|
||||
Error::Daemon(error)
|
||||
}
|
||||
}
|
||||
4
gui/src/app/menu.rs
Normal file
4
gui/src/app/menu.rs
Normal file
@ -0,0 +1,4 @@
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Menu {
|
||||
Home,
|
||||
}
|
||||
14
gui/src/app/message.rs
Normal file
14
gui/src/app/message.rs
Normal file
@ -0,0 +1,14 @@
|
||||
use minisafe::config::Config as DaemonConfig;
|
||||
|
||||
use crate::app::{error::Error, menu::Menu};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
Reload,
|
||||
Tick,
|
||||
Event(iced_native::Event),
|
||||
Clipboard(String),
|
||||
Menu(Menu),
|
||||
LoadDaemonConfig(Box<DaemonConfig>),
|
||||
DaemonConfigLoaded(Result<(), Error>),
|
||||
}
|
||||
96
gui/src/app/mod.rs
Normal file
96
gui/src/app/mod.rs
Normal file
@ -0,0 +1,96 @@
|
||||
pub mod config;
|
||||
pub mod context;
|
||||
pub mod menu;
|
||||
pub mod message;
|
||||
pub mod state;
|
||||
|
||||
mod error;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use iced::pure::Element;
|
||||
use iced::{clipboard, time, Command, Subscription};
|
||||
use iced_native::{window, Event};
|
||||
|
||||
pub use config::Config;
|
||||
pub use message::Message;
|
||||
|
||||
use state::{Home, State};
|
||||
|
||||
use crate::app::context::Context;
|
||||
|
||||
pub struct App {
|
||||
should_exit: bool,
|
||||
state: Box<dyn State>,
|
||||
context: Context,
|
||||
}
|
||||
|
||||
pub fn new_state(_context: &Context) -> Box<dyn State> {
|
||||
Home {}.into()
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(context: Context) -> (App, Command<Message>) {
|
||||
let state = new_state(&context);
|
||||
let cmd = state.load(&context);
|
||||
(
|
||||
Self {
|
||||
should_exit: false,
|
||||
state,
|
||||
context,
|
||||
},
|
||||
cmd,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn subscription(&self) -> Subscription<Message> {
|
||||
Subscription::batch(vec![
|
||||
iced_native::subscription::events().map(Message::Event),
|
||||
time::every(Duration::from_secs(30)).map(|_| Message::Tick),
|
||||
self.state.subscription(),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn should_exit(&self) -> bool {
|
||||
self.should_exit
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) {
|
||||
log::info!("Close requested");
|
||||
if !self.context.daemon.is_external() {
|
||||
log::info!("Stopping internal daemon...");
|
||||
if let Some(d) = Arc::get_mut(&mut self.context.daemon) {
|
||||
d.stop().expect("Daemon is internal");
|
||||
log::info!("Internal daemon stopped");
|
||||
self.should_exit = true;
|
||||
}
|
||||
} else {
|
||||
self.should_exit = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, message: Message) -> Command<Message> {
|
||||
match message {
|
||||
Message::LoadDaemonConfig(cfg) => {
|
||||
let res = self.context.load_daemon_config(*cfg);
|
||||
self.update(Message::DaemonConfigLoaded(res))
|
||||
}
|
||||
Message::Menu(menu) => {
|
||||
self.context.menu = menu;
|
||||
self.state = new_state(&self.context);
|
||||
self.state.load(&self.context)
|
||||
}
|
||||
Message::Clipboard(text) => clipboard::write(text),
|
||||
Message::Event(Event::Window(window::Event::CloseRequested)) => {
|
||||
self.stop();
|
||||
Command::none()
|
||||
}
|
||||
_ => self.state.update(&self.context, message),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(&self) -> Element<Message> {
|
||||
self.state.view(&self.context)
|
||||
}
|
||||
}
|
||||
32
gui/src/app/state.rs
Normal file
32
gui/src/app/state.rs
Normal file
@ -0,0 +1,32 @@
|
||||
use iced::pure::{column, Element};
|
||||
use iced::{Command, Subscription};
|
||||
|
||||
use super::{context::Context, message::Message};
|
||||
|
||||
pub trait State {
|
||||
fn view(&self, ctx: &Context) -> Element<Message>;
|
||||
fn update(&mut self, ctx: &Context, message: Message) -> Command<Message>;
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
Subscription::none()
|
||||
}
|
||||
fn load(&self, _ctx: &Context) -> Command<Message> {
|
||||
Command::none()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Home {}
|
||||
|
||||
impl State for Home {
|
||||
fn view(&self, _ctx: &Context) -> Element<Message> {
|
||||
column().into()
|
||||
}
|
||||
fn update(&mut self, _ctx: &Context, _message: Message) -> Command<Message> {
|
||||
Command::none()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Home> for Box<dyn State> {
|
||||
fn from(s: Home) -> Box<dyn State> {
|
||||
Box::new(s)
|
||||
}
|
||||
}
|
||||
43
gui/src/conversion.rs
Normal file
43
gui/src/conversion.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use bitcoin::Network;
|
||||
|
||||
/// Converter purpose is to give a Conversion from a given amount in satoshis according to its
|
||||
/// parameters.
|
||||
pub struct Converter {
|
||||
pub unit: Unit,
|
||||
}
|
||||
|
||||
impl Converter {
|
||||
pub fn new(bitcoin_network: Network) -> Self {
|
||||
let unit = match bitcoin_network {
|
||||
Network::Testnet => Unit::TestnetBitcoin,
|
||||
Network::Bitcoin => Unit::Bitcoin,
|
||||
Network::Regtest => Unit::RegtestBitcoin,
|
||||
Network::Signet => Unit::SignetBitcoin,
|
||||
};
|
||||
Self { unit }
|
||||
}
|
||||
|
||||
/// converts amount in satoshis to BTC float.
|
||||
pub fn converts(&self, amount: bitcoin::Amount) -> String {
|
||||
format!("{:.8}", amount.as_btc())
|
||||
}
|
||||
}
|
||||
|
||||
/// Unit is the bitcoin ticker according to the network used.
|
||||
pub enum Unit {
|
||||
TestnetBitcoin,
|
||||
RegtestBitcoin,
|
||||
SignetBitcoin,
|
||||
Bitcoin,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Unit {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::TestnetBitcoin => write!(f, "tBTC"),
|
||||
Self::RegtestBitcoin => write!(f, "rBTC"),
|
||||
Self::SignetBitcoin => write!(f, "sBTC"),
|
||||
Self::Bitcoin => write!(f, "BTC"),
|
||||
}
|
||||
}
|
||||
}
|
||||
104
gui/src/daemon/client/error.rs
Normal file
104
gui/src/daemon/client/error.rs
Normal file
@ -0,0 +1,104 @@
|
||||
// Rust JSON-RPC Library
|
||||
// Written in 2015 by
|
||||
// Andrew Poelstra <apoelstra@wpsoftware.net>
|
||||
//
|
||||
// To the extent possible under law, the author(s) have dedicated all
|
||||
// copyright and related and neighboring rights to this software to
|
||||
// the public domain worldwide. This software is distributed without
|
||||
// any warranty.
|
||||
//
|
||||
// You should have received a copy of the CC0 Public Domain Dedication
|
||||
// along with this software.
|
||||
// If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
|
||||
//
|
||||
|
||||
//! Error handling
|
||||
//!
|
||||
//! Some useful methods for creating Error objects
|
||||
//!
|
||||
|
||||
use std::io;
|
||||
use std::{error, fmt};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum RpcErrorCode {
|
||||
// Standard errors defined by JSON-RPC 2.0 standard
|
||||
/// Invalid request
|
||||
JSONRPC2_INVALID_REQUEST = -32600,
|
||||
/// Method not found
|
||||
JSONRPC2_METHOD_NOT_FOUND = -32601,
|
||||
/// Invalid parameters
|
||||
JSONRPC2_INVALID_PARAMS = -32602,
|
||||
}
|
||||
|
||||
/// A library error
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Json error
|
||||
Json(serde_json::Error),
|
||||
/// IO Error
|
||||
Io(io::Error),
|
||||
/// Error response
|
||||
Rpc(RpcError),
|
||||
/// Response has neither error nor result
|
||||
NoErrorOrResult,
|
||||
/// Response to a request did not have the expected nonce
|
||||
NonceMismatch,
|
||||
/// Response to a request had a jsonrpc field other than "2.0"
|
||||
VersionMismatch,
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(e: serde_json::Error) -> Error {
|
||||
Error::Json(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(e: io::Error) -> Error {
|
||||
Error::Io(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RpcError> for Error {
|
||||
fn from(e: RpcError) -> Error {
|
||||
Error::Rpc(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
Error::Json(ref e) => write!(f, "JSON decode error: {}", e),
|
||||
Error::Io(ref e) => write!(f, "IO error response: {}", e),
|
||||
Error::Rpc(ref r) => write!(f, "RPC error response: {:?}", r),
|
||||
Error::NoErrorOrResult => write!(f, "Malformed RPC response"),
|
||||
Error::NonceMismatch => write!(f, "Nonce of response did not match nonce of request"),
|
||||
Error::VersionMismatch => write!(f, "`jsonrpc` field set to non-\"2.0\""),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for Error {
|
||||
fn cause(&self) -> Option<&dyn error::Error> {
|
||||
match *self {
|
||||
Error::Json(ref e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
||||
/// A JSONRPC error object
|
||||
pub struct RpcError {
|
||||
/// The integer identifier of the error
|
||||
pub code: i32,
|
||||
/// A string describing the error
|
||||
pub message: String,
|
||||
/// Additional data specific to the error
|
||||
pub data: Option<serde_json::Value>,
|
||||
}
|
||||
255
gui/src/daemon/client/jsonrpc.rs
Normal file
255
gui/src/daemon/client/jsonrpc.rs
Normal file
@ -0,0 +1,255 @@
|
||||
// Rust JSON-RPC Library
|
||||
// Written by
|
||||
// Andrew Poelstra <apoelstra@wpsoftware.net>
|
||||
// Wladimir J. van der Laan <laanwj@gmail.com>
|
||||
//
|
||||
// To the extent possible under law, the author(s) have dedicated all
|
||||
// copyright and related and neighboring rights to this software to
|
||||
// the public domain worldwide. This software is distributed without
|
||||
// any warranty.
|
||||
//
|
||||
// You should have received a copy of the CC0 Public Domain Dedication
|
||||
// along with this software.
|
||||
// If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
|
||||
//
|
||||
//! Client support
|
||||
//!
|
||||
//! Support for connecting to JSONRPC servers over UNIX socets, sending requests,
|
||||
//! and parsing responses
|
||||
//!
|
||||
|
||||
#[cfg(windows)]
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
use std::fmt::Debug;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
use std::{error, fmt, io};
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{to_writer, Deserializer};
|
||||
|
||||
use log::debug;
|
||||
|
||||
/// A handle to a remote JSONRPC server
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JsonRPCClient {
|
||||
sockpath: PathBuf,
|
||||
timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
impl super::Client for JsonRPCClient {
|
||||
type Error = Error;
|
||||
fn request<S: Serialize + Debug, D: DeserializeOwned + Debug>(
|
||||
&self,
|
||||
method: &str,
|
||||
params: Option<S>,
|
||||
) -> Result<D, Self::Error> {
|
||||
self.send_request(method, params)
|
||||
.and_then(|res| res.into_result())
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonRPCClient {
|
||||
/// Creates a new client
|
||||
pub fn new<P: AsRef<Path>>(sockpath: P) -> JsonRPCClient {
|
||||
JsonRPCClient {
|
||||
sockpath: sockpath.as_ref().to_path_buf(),
|
||||
timeout: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set an optional timeout for requests
|
||||
#[allow(dead_code)]
|
||||
pub fn set_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.timeout = timeout;
|
||||
}
|
||||
|
||||
/// Sends a request to a client
|
||||
pub fn send_request<S: Serialize + Debug, D: DeserializeOwned + Debug>(
|
||||
&self,
|
||||
method: &str,
|
||||
params: Option<S>,
|
||||
) -> Result<Response<D>, Error> {
|
||||
// Setup connection
|
||||
let mut stream = UnixStream::connect(&self.sockpath)?;
|
||||
stream.set_read_timeout(self.timeout)?;
|
||||
stream.set_write_timeout(self.timeout)?;
|
||||
|
||||
let request = Request {
|
||||
method,
|
||||
params,
|
||||
id: std::process::id(),
|
||||
jsonrpc: "2.0",
|
||||
};
|
||||
|
||||
debug!("Sending to minisafed: {:#?}", request);
|
||||
|
||||
to_writer(&mut stream, &request)?;
|
||||
|
||||
let response: Response<D> = Deserializer::from_reader(&mut stream)
|
||||
.into_iter()
|
||||
.next()
|
||||
.map_or(Err(Error::NoErrorOrResult), |res| Ok(res?))?;
|
||||
if response
|
||||
.jsonrpc
|
||||
.as_ref()
|
||||
.map_or(false, |version| version != "2.0")
|
||||
{
|
||||
return Err(Error::VersionMismatch);
|
||||
}
|
||||
|
||||
if response.id != request.id {
|
||||
return Err(Error::NonceMismatch);
|
||||
}
|
||||
|
||||
debug!("Received from minisafed: {:#?}", response);
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||
/// A JSONRPC request object
|
||||
pub struct Request<'f, T: Serialize> {
|
||||
/// The name of the RPC call
|
||||
pub method: &'f str,
|
||||
/// Parameters to the RPC call
|
||||
pub params: Option<T>,
|
||||
/// Identifier for this Request, which should appear in the response
|
||||
pub id: u32,
|
||||
/// jsonrpc field, MUST be "2.0"
|
||||
pub jsonrpc: &'f str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
/// A JSONRPC response object
|
||||
pub struct Response<T> {
|
||||
/// A result if there is one, or null
|
||||
pub result: Option<T>,
|
||||
/// An error if there is one, or null
|
||||
pub error: Option<RpcError>,
|
||||
/// Identifier for this Request, which should match that of the request
|
||||
pub id: u32,
|
||||
/// jsonrpc field, MUST be "2.0"
|
||||
pub jsonrpc: Option<String>,
|
||||
}
|
||||
|
||||
impl<T> Response<T> {
|
||||
/// Extract the result from a response, consuming the response
|
||||
pub fn into_result(self) -> Result<T, Error> {
|
||||
if let Some(e) = self.error {
|
||||
return Err(Error::Rpc(e));
|
||||
}
|
||||
|
||||
self.result.ok_or(Error::NoErrorOrResult)
|
||||
}
|
||||
|
||||
/// Returns whether or not the `result` field is empty
|
||||
#[allow(dead_code)]
|
||||
pub fn is_none(&self) -> bool {
|
||||
self.result.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum RpcErrorCode {
|
||||
// Standard errors defined by JSON-RPC 2.0 standard
|
||||
/// Invalid request
|
||||
JSONRPC2_INVALID_REQUEST = -32600,
|
||||
/// Method not found
|
||||
JSONRPC2_METHOD_NOT_FOUND = -32601,
|
||||
/// Invalid parameters
|
||||
JSONRPC2_INVALID_PARAMS = -32602,
|
||||
}
|
||||
|
||||
/// A library error
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Json error
|
||||
Json(serde_json::Error),
|
||||
/// IO Error
|
||||
Io(io::Error),
|
||||
/// Error response
|
||||
Rpc(RpcError),
|
||||
/// Response has neither error nor result
|
||||
NoErrorOrResult,
|
||||
/// Response to a request did not have the expected nonce
|
||||
NonceMismatch,
|
||||
/// Response to a request had a jsonrpc field other than "2.0"
|
||||
VersionMismatch,
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(e: serde_json::Error) -> Error {
|
||||
Error::Json(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(e: io::Error) -> Error {
|
||||
Error::Io(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RpcError> for Error {
|
||||
fn from(e: RpcError) -> Error {
|
||||
Error::Rpc(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
Error::Json(ref e) => write!(f, "JSON decode error: {}", e),
|
||||
Error::Io(ref e) => write!(f, "IO error response: {}", e),
|
||||
Error::Rpc(ref r) => write!(f, "RPC error response: {:?}", r),
|
||||
Error::NoErrorOrResult => write!(f, "Malformed RPC response"),
|
||||
Error::NonceMismatch => write!(f, "Nonce of response did not match nonce of request"),
|
||||
Error::VersionMismatch => write!(f, "`jsonrpc` field set to non-\"2.0\""),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for Error {
|
||||
fn cause(&self) -> Option<&dyn error::Error> {
|
||||
match *self {
|
||||
Error::Json(ref e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for super::DaemonError {
|
||||
fn from(e: Error) -> super::DaemonError {
|
||||
match e {
|
||||
Error::Io(e) => super::DaemonError::Transport(Some(e.kind()), format!("io: {:?}", e)),
|
||||
Error::Json(e) => super::DaemonError::Transport(None, format!("json decode: {}", e)),
|
||||
Error::NonceMismatch => {
|
||||
super::DaemonError::Transport(None, format!("transport: {}", e))
|
||||
}
|
||||
Error::VersionMismatch => {
|
||||
super::DaemonError::Transport(None, format!("transport: {}", e))
|
||||
}
|
||||
Error::NoErrorOrResult => super::DaemonError::NoAnswer,
|
||||
Error::Rpc(e) => super::DaemonError::Rpc(e.code, e.message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
||||
/// A JSONRPC error object
|
||||
pub struct RpcError {
|
||||
/// The integer identifier of the error
|
||||
pub code: i32,
|
||||
/// A string describing the error
|
||||
pub message: String,
|
||||
/// Additional data specific to the error
|
||||
pub data: Option<serde_json::Value>,
|
||||
}
|
||||
61
gui/src/daemon/client/mod.rs
Normal file
61
gui/src/daemon/client/mod.rs
Normal file
@ -0,0 +1,61 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use log::{error, info};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod error;
|
||||
pub mod jsonrpc;
|
||||
|
||||
use super::{model::*, Daemon, DaemonError};
|
||||
|
||||
pub trait Client {
|
||||
type Error: Into<DaemonError> + Debug;
|
||||
fn request<S: Serialize + Debug, D: DeserializeOwned + Debug>(
|
||||
&self,
|
||||
method: &str,
|
||||
params: Option<S>,
|
||||
) -> Result<D, Self::Error>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Minisafed<C: Client> {
|
||||
client: C,
|
||||
}
|
||||
|
||||
impl<C: Client> Minisafed<C> {
|
||||
pub fn new(client: C) -> Minisafed<C> {
|
||||
Minisafed { client }
|
||||
}
|
||||
|
||||
/// Generic call function for RPC calls.
|
||||
fn call<T: Serialize + Debug, U: DeserializeOwned + Debug>(
|
||||
&self,
|
||||
method: &str,
|
||||
input: Option<T>,
|
||||
) -> Result<U, DaemonError> {
|
||||
info!("{}", method);
|
||||
self.client.request(method, input).map_err(|e| {
|
||||
error!("method {} failed: {:?}", method, e);
|
||||
e.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Client + Debug> Daemon for Minisafed<C> {
|
||||
fn is_external(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn stop(&mut self) -> Result<(), DaemonError> {
|
||||
let _res: serde_json::value::Value = self.call("stop", Option::<Request>::None)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_info(&self) -> Result<GetInfoResult, DaemonError> {
|
||||
self.call("getinfo", Option::<Request>::None)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Request {}
|
||||
61
gui/src/daemon/embedded.rs
Normal file
61
gui/src/daemon/embedded.rs
Normal file
@ -0,0 +1,61 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use super::{model::*, Daemon, DaemonError};
|
||||
use minisafe::{config::Config, DaemonHandle};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EmbeddedDaemon {
|
||||
handle: Option<Mutex<DaemonHandle>>,
|
||||
}
|
||||
|
||||
impl EmbeddedDaemon {
|
||||
pub fn start(&mut self, config: Config) -> Result<(), DaemonError> {
|
||||
let handle =
|
||||
DaemonHandle::start_default(config).map_err(|e| DaemonError::Start(e.to_string()))?;
|
||||
self.handle = Some(Mutex::new(handle));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for EmbeddedDaemon {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("DaemonHandle").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Daemon for EmbeddedDaemon {
|
||||
fn is_external(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn load_config(&mut self, cfg: Config) -> Result<(), DaemonError> {
|
||||
if self.handle.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let next =
|
||||
DaemonHandle::start_default(cfg).map_err(|e| DaemonError::Start(e.to_string()))?;
|
||||
self.handle.take().unwrap().into_inner().unwrap().shutdown();
|
||||
self.handle = Some(Mutex::new(next));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop(&mut self) -> Result<(), DaemonError> {
|
||||
if let Some(h) = self.handle.take() {
|
||||
let handle = h.into_inner().unwrap();
|
||||
handle.shutdown();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_info(&self) -> Result<GetInfoResult, DaemonError> {
|
||||
Ok(self
|
||||
.handle
|
||||
.as_ref()
|
||||
.ok_or(DaemonError::NoAnswer)?
|
||||
.lock()
|
||||
.unwrap()
|
||||
.control
|
||||
.get_info())
|
||||
}
|
||||
}
|
||||
46
gui/src/daemon/mod.rs
Normal file
46
gui/src/daemon/mod.rs
Normal file
@ -0,0 +1,46 @@
|
||||
pub mod client;
|
||||
pub mod embedded;
|
||||
pub mod model;
|
||||
|
||||
use std::fmt::Debug;
|
||||
use std::io::ErrorKind;
|
||||
|
||||
use minisafe::config::Config;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DaemonError {
|
||||
/// Something was wrong with the request.
|
||||
Rpc(i32, String),
|
||||
/// Something was wrong with the communication.
|
||||
Transport(Option<ErrorKind>, String),
|
||||
/// Something unexpected happened.
|
||||
Unexpected(String),
|
||||
/// No response.
|
||||
NoAnswer,
|
||||
// Error at start up.
|
||||
Start(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DaemonError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Rpc(code, e) => write!(f, "Daemon error rpc call: [{:?}] {}", code, e),
|
||||
Self::NoAnswer => write!(f, "Daemon returned no answer"),
|
||||
Self::Transport(kind, e) => write!(f, "Daemon transport error: [{:?}] {}", kind, e),
|
||||
Self::Unexpected(e) => write!(f, "Daemon unexpected error: {}", e),
|
||||
Self::Start(e) => write!(f, "Daemon did not start: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Daemon: Debug {
|
||||
fn is_external(&self) -> bool;
|
||||
|
||||
fn load_config(&mut self, _cfg: Config) -> Result<(), DaemonError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop(&mut self) -> Result<(), DaemonError>;
|
||||
|
||||
fn get_info(&self) -> Result<model::GetInfoResult, DaemonError>;
|
||||
}
|
||||
1
gui/src/daemon/model.rs
Normal file
1
gui/src/daemon/model.rs
Normal file
@ -0,0 +1 @@
|
||||
pub use minisafe::commands::GetInfoResult;
|
||||
80
gui/src/installer/config.rs
Normal file
80
gui/src/installer/config.rs
Normal file
@ -0,0 +1,80 @@
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use bitcoin::Network;
|
||||
use minisafe::{
|
||||
config::{BitcoinConfig, BitcoindConfig, Config as MinisafeConfig},
|
||||
miniscript::{Descriptor, DescriptorPublicKey},
|
||||
};
|
||||
|
||||
use serde::Serialize;
|
||||
use std::{net::SocketAddr, path::PathBuf, time::Duration};
|
||||
|
||||
/// Static informations we require to operate
|
||||
/// fields with default values are not present, see minisafe::config.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct Config {
|
||||
#[serde(serialize_with = "serialize_option_to_string")]
|
||||
pub main_descriptor: Option<Descriptor<DescriptorPublicKey>>,
|
||||
pub bitcoin_config: BitcoinConfig,
|
||||
/// Everything we need to know to talk to bitcoind
|
||||
pub bitcoind_config: BitcoindConfig,
|
||||
/// An optional custom data directory
|
||||
pub data_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub const DEFAULT_FILE_NAME: &'static str = "daemon.toml";
|
||||
/// returns a minisafed config with empty or dummy values
|
||||
pub fn new() -> Config {
|
||||
Self {
|
||||
main_descriptor: None,
|
||||
bitcoin_config: BitcoinConfig {
|
||||
network: Network::Bitcoin,
|
||||
poll_interval_secs: Duration::from_secs(30),
|
||||
},
|
||||
bitcoind_config: BitcoindConfig {
|
||||
cookie_path: PathBuf::new(),
|
||||
addr: SocketAddr::new(
|
||||
std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)),
|
||||
8080,
|
||||
),
|
||||
},
|
||||
data_dir: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_option_to_string<T: std::fmt::Display, S: serde::Serializer>(
|
||||
field: &Option<T>,
|
||||
s: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
match field {
|
||||
Some(field) => s.serialize_str(&field.to_string()),
|
||||
None => s.serialize_none(),
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Config> for MinisafeConfig {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(cfg: Config) -> Result<Self, Self::Error> {
|
||||
if cfg.main_descriptor.is_none() {
|
||||
return Err("config does not have a main Descriptor");
|
||||
}
|
||||
Ok(MinisafeConfig {
|
||||
#[cfg(unix)]
|
||||
daemon: false,
|
||||
log_level: log::LevelFilter::Info,
|
||||
main_descriptor: cfg.main_descriptor.unwrap(),
|
||||
data_dir: cfg.data_dir,
|
||||
bitcoin_config: cfg.bitcoin_config,
|
||||
bitcoind_config: Some(cfg.bitcoind_config),
|
||||
})
|
||||
}
|
||||
}
|
||||
30
gui/src/installer/message.rs
Normal file
30
gui/src/installer/message.rs
Normal file
@ -0,0 +1,30 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::Error;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
Event(iced_native::Event),
|
||||
Exit(PathBuf),
|
||||
Next,
|
||||
Previous,
|
||||
Install,
|
||||
Installed(Result<PathBuf, Error>),
|
||||
Network(bitcoin::Network),
|
||||
DefineBitcoind(DefineBitcoind),
|
||||
DefineDescriptor(DefineDescriptor),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DefineBitcoind {
|
||||
CookiePathEdited(String),
|
||||
AddressEdited(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DefineDescriptor {
|
||||
ImportDescriptor(String),
|
||||
UserXpubEdited(String),
|
||||
HeirXpubEdited(String),
|
||||
SequenceEdited(String),
|
||||
}
|
||||
209
gui/src/installer/mod.rs
Normal file
209
gui/src/installer/mod.rs
Normal file
@ -0,0 +1,209 @@
|
||||
mod config;
|
||||
mod message;
|
||||
mod step;
|
||||
mod view;
|
||||
|
||||
use iced::pure::Element;
|
||||
use iced::{Command, Subscription};
|
||||
use iced_native::{window, Event};
|
||||
|
||||
use std::convert::TryInto;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::{app::config as gui_config, installer::config::Config as DaemonConfig};
|
||||
|
||||
pub use message::Message;
|
||||
use step::{Context, DefineBitcoind, DefineDescriptor, Final, Step, Welcome};
|
||||
|
||||
pub struct Installer {
|
||||
should_exit: bool,
|
||||
current: usize,
|
||||
steps: Vec<Box<dyn Step>>,
|
||||
|
||||
/// Context is data passed through each step.
|
||||
context: Context,
|
||||
config: DaemonConfig,
|
||||
}
|
||||
|
||||
impl Installer {
|
||||
fn next(&mut self) {
|
||||
if self.current < self.steps.len() - 1 {
|
||||
self.current += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn previous(&mut self) {
|
||||
if self.current > 0 {
|
||||
self.current -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
destination_path: PathBuf,
|
||||
network: bitcoin::Network,
|
||||
) -> (Installer, Command<Message>) {
|
||||
let mut config = DaemonConfig::new();
|
||||
config.data_dir = Some(destination_path);
|
||||
(
|
||||
Installer {
|
||||
should_exit: false,
|
||||
config,
|
||||
current: 0,
|
||||
steps: vec![
|
||||
Welcome::new(network).into(),
|
||||
DefineDescriptor::new().into(),
|
||||
DefineBitcoind::new().into(),
|
||||
Final::new().into(),
|
||||
],
|
||||
context: Context::new(network),
|
||||
},
|
||||
Command::none(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn subscription(&self) -> Subscription<Message> {
|
||||
iced_native::subscription::events().map(Message::Event)
|
||||
}
|
||||
|
||||
pub fn should_exit(&self) -> bool {
|
||||
self.should_exit
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) {
|
||||
self.should_exit = true;
|
||||
}
|
||||
|
||||
pub fn update(&mut self, message: Message) -> Command<Message> {
|
||||
match message {
|
||||
Message::Next => {
|
||||
let current_step = self
|
||||
.steps
|
||||
.get_mut(self.current)
|
||||
.expect("There is always a step");
|
||||
if current_step.apply(&mut self.context, &mut self.config) {
|
||||
self.next();
|
||||
// skip the step according to the current context.
|
||||
while self
|
||||
.steps
|
||||
.get(self.current)
|
||||
.expect("There is always a step")
|
||||
.skip(&self.context)
|
||||
{
|
||||
self.next();
|
||||
}
|
||||
// calculate new current_step.
|
||||
let current_step = self
|
||||
.steps
|
||||
.get_mut(self.current)
|
||||
.expect("There is always a step");
|
||||
current_step.load_context(&self.context);
|
||||
}
|
||||
}
|
||||
Message::Previous => {
|
||||
self.previous();
|
||||
}
|
||||
Message::Install => {
|
||||
self.steps
|
||||
.get_mut(self.current)
|
||||
.expect("There is always a step")
|
||||
.update(message);
|
||||
return Command::perform(
|
||||
install(self.context.clone(), self.config.clone()),
|
||||
Message::Installed,
|
||||
);
|
||||
}
|
||||
Message::Event(Event::Window(window::Event::CloseRequested)) => {
|
||||
self.stop();
|
||||
return Command::none();
|
||||
}
|
||||
_ => {
|
||||
self.steps
|
||||
.get_mut(self.current)
|
||||
.expect("There is always a step")
|
||||
.update(message);
|
||||
}
|
||||
};
|
||||
Command::none()
|
||||
}
|
||||
|
||||
pub fn view(&self) -> Element<Message> {
|
||||
self.steps
|
||||
.get(self.current)
|
||||
.expect("There is always a step")
|
||||
.view()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn install(_ctx: Context, mut cfg: DaemonConfig) -> Result<PathBuf, Error> {
|
||||
// Start Daemon to check correctness of installation
|
||||
let daemon =
|
||||
minisafe::DaemonHandle::start_default(cfg.clone().try_into().unwrap()).map_err(|e| {
|
||||
Error::Unexpected(format!("Failed to start daemon with entered config: {}", e))
|
||||
})?;
|
||||
daemon.shutdown();
|
||||
|
||||
cfg.data_dir =
|
||||
Some(cfg.data_dir.unwrap().canonicalize().map_err(|e| {
|
||||
Error::Unexpected(format!("Failed to canonicalize datadir path: {}", e))
|
||||
})?);
|
||||
|
||||
let mut datadir_path = cfg.data_dir.clone().unwrap();
|
||||
datadir_path.push(cfg.bitcoin_config.network.to_string());
|
||||
|
||||
// create minisafed configuration file
|
||||
let mut minisafed_config_path = datadir_path.clone();
|
||||
minisafed_config_path.push(DaemonConfig::DEFAULT_FILE_NAME);
|
||||
let mut minisafed_config_file = std::fs::File::create(&minisafed_config_path)
|
||||
.map_err(|e| Error::CannotCreateFile(e.to_string()))?;
|
||||
|
||||
// Step needed because of ValueAfterTable error in the toml serialize implementation.
|
||||
let minisafed_config =
|
||||
toml::Value::try_from(&cfg).expect("daemon::Config has a proper Serialize implementation");
|
||||
|
||||
minisafed_config_file
|
||||
.write_all(minisafed_config.to_string().as_bytes())
|
||||
.map_err(|e| Error::CannotWriteToFile(e.to_string()))?;
|
||||
|
||||
// create minisafe GUI configuration file
|
||||
let mut gui_config_path = datadir_path;
|
||||
gui_config_path.push(gui_config::DEFAULT_FILE_NAME);
|
||||
let mut gui_config_file = std::fs::File::create(&gui_config_path)
|
||||
.map_err(|e| Error::CannotCreateFile(e.to_string()))?;
|
||||
|
||||
gui_config_file
|
||||
.write_all(
|
||||
toml::to_string(&gui_config::Config::new(
|
||||
minisafed_config_path.canonicalize().map_err(|e| {
|
||||
Error::Unexpected(format!(
|
||||
"Failed to canonicalize minisafed config path: {}",
|
||||
e
|
||||
))
|
||||
})?,
|
||||
))
|
||||
.unwrap()
|
||||
.as_bytes(),
|
||||
)
|
||||
.map_err(|e| Error::CannotWriteToFile(e.to_string()))?;
|
||||
|
||||
Ok(gui_config_path)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Error {
|
||||
CannotCreateDatadir(String),
|
||||
CannotCreateFile(String),
|
||||
CannotWriteToFile(String),
|
||||
Unexpected(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::CannotCreateDatadir(e) => write!(f, "Failed to create datadir: {}", e),
|
||||
Self::CannotWriteToFile(e) => write!(f, "Failed to write to file: {}", e),
|
||||
Self::CannotCreateFile(e) => write!(f, "Failed to create file: {}", e),
|
||||
Self::Unexpected(e) => write!(f, "Unexpected: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
396
gui/src/installer/step/mod.rs
Normal file
396
gui/src/installer/step/mod.rs
Normal file
@ -0,0 +1,396 @@
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use iced::pure::Element;
|
||||
use minisafe::{
|
||||
descriptors::inheritance_descriptor,
|
||||
miniscript::descriptor::{Descriptor, DescriptorPublicKey},
|
||||
};
|
||||
|
||||
use crate::ui::component::form;
|
||||
|
||||
use crate::installer::{
|
||||
config,
|
||||
message::{self, Message},
|
||||
view,
|
||||
};
|
||||
|
||||
pub trait Step {
|
||||
fn update(&mut self, message: Message);
|
||||
fn view(&self) -> Element<Message>;
|
||||
fn load_context(&mut self, _ctx: &Context) {}
|
||||
fn skip(&self, _ctx: &Context) -> bool {
|
||||
false
|
||||
}
|
||||
fn apply(&mut self, _ctx: &mut Context, _config: &mut config::Config) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Context {
|
||||
pub network: bitcoin::Network,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn new(network: bitcoin::Network) -> Self {
|
||||
Self { network }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Context {
|
||||
fn default() -> Self {
|
||||
Self::new(bitcoin::Network::Bitcoin)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Welcome {
|
||||
network: bitcoin::Network,
|
||||
}
|
||||
|
||||
impl Welcome {
|
||||
pub fn new(network: bitcoin::Network) -> Self {
|
||||
Self { network }
|
||||
}
|
||||
}
|
||||
|
||||
impl Step for Welcome {
|
||||
fn update(&mut self, message: Message) {
|
||||
if let message::Message::Network(network) = message {
|
||||
self.network = network;
|
||||
}
|
||||
}
|
||||
fn apply(&mut self, ctx: &mut Context, config: &mut config::Config) -> bool {
|
||||
ctx.network = self.network;
|
||||
config.bitcoin_config.network = self.network;
|
||||
true
|
||||
}
|
||||
fn view(&self) -> Element<Message> {
|
||||
view::welcome(&self.network)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Welcome {
|
||||
fn default() -> Self {
|
||||
Self::new(bitcoin::Network::Bitcoin)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Welcome> for Box<dyn Step> {
|
||||
fn from(s: Welcome) -> Box<dyn Step> {
|
||||
Box::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DefineDescriptor {
|
||||
imported_descriptor: form::Value<String>,
|
||||
user_xpub: form::Value<String>,
|
||||
heir_xpub: form::Value<String>,
|
||||
sequence: form::Value<String>,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
impl DefineDescriptor {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
imported_descriptor: form::Value::default(),
|
||||
user_xpub: form::Value::default(),
|
||||
heir_xpub: form::Value::default(),
|
||||
sequence: form::Value::default(),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Step for DefineDescriptor {
|
||||
// form value is set as valid each time it is edited.
|
||||
// Verification of the values is happening when the user click on Next button.
|
||||
fn update(&mut self, message: Message) {
|
||||
if let Message::DefineDescriptor(msg) = message {
|
||||
match msg {
|
||||
message::DefineDescriptor::ImportDescriptor(desc) => {
|
||||
self.imported_descriptor.value = desc;
|
||||
self.imported_descriptor.valid = true;
|
||||
}
|
||||
message::DefineDescriptor::UserXpubEdited(xpub) => {
|
||||
self.user_xpub.value = xpub;
|
||||
self.user_xpub.valid = true;
|
||||
}
|
||||
message::DefineDescriptor::HeirXpubEdited(xpub) => {
|
||||
self.heir_xpub.value = xpub;
|
||||
self.heir_xpub.valid = true;
|
||||
}
|
||||
message::DefineDescriptor::SequenceEdited(seq) => {
|
||||
self.sequence.valid = true;
|
||||
if seq.is_empty() || seq.parse::<u32>().is_ok() {
|
||||
self.sequence.value = seq;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
fn apply(&mut self, _ctx: &mut Context, config: &mut config::Config) -> bool {
|
||||
// descriptor forms for import or creation cannot be both empty or filled.
|
||||
if self.imported_descriptor.value.is_empty()
|
||||
== (self.user_xpub.value.is_empty()
|
||||
|| self.heir_xpub.value.is_empty()
|
||||
|| self.sequence.value.is_empty())
|
||||
{
|
||||
if !self.user_xpub.value.is_empty() {
|
||||
self.user_xpub.valid = DescriptorPublicKey::from_str(&self.user_xpub.value).is_ok();
|
||||
}
|
||||
if !self.heir_xpub.value.is_empty() {
|
||||
self.heir_xpub.valid = DescriptorPublicKey::from_str(&self.heir_xpub.value).is_ok();
|
||||
}
|
||||
if !self.sequence.value.is_empty() {
|
||||
self.sequence.valid = self.sequence.value.parse::<u32>().is_ok();
|
||||
}
|
||||
if !self.imported_descriptor.value.is_empty() {
|
||||
self.imported_descriptor.valid =
|
||||
Descriptor::<DescriptorPublicKey>::from_str(&self.imported_descriptor.value)
|
||||
.is_ok();
|
||||
}
|
||||
false
|
||||
} else if !self.imported_descriptor.value.is_empty() {
|
||||
if let Ok(desc) =
|
||||
Descriptor::<DescriptorPublicKey>::from_str(&self.imported_descriptor.value)
|
||||
{
|
||||
config.main_descriptor = Some(desc);
|
||||
true
|
||||
} else {
|
||||
self.imported_descriptor.valid = false;
|
||||
false
|
||||
}
|
||||
} else {
|
||||
let user_key = DescriptorPublicKey::from_str(&self.user_xpub.value);
|
||||
self.user_xpub.valid = user_key.is_ok();
|
||||
|
||||
let heir_key = DescriptorPublicKey::from_str(&self.heir_xpub.value);
|
||||
self.user_xpub.valid = user_key.is_ok();
|
||||
|
||||
let sequence = self.sequence.value.parse::<u32>();
|
||||
self.sequence.valid = sequence.is_ok();
|
||||
|
||||
if !self.user_xpub.valid || !self.heir_xpub.valid || !self.sequence.valid {
|
||||
return false;
|
||||
}
|
||||
|
||||
match inheritance_descriptor(user_key.unwrap(), heir_key.unwrap(), sequence.unwrap()) {
|
||||
Ok(desc) => {
|
||||
config.main_descriptor = Some(desc);
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
self.error = Some(e.to_string());
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
view::define_descriptor(
|
||||
&self.imported_descriptor,
|
||||
&self.user_xpub,
|
||||
&self.heir_xpub,
|
||||
&self.sequence,
|
||||
self.error.as_ref(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DefineDescriptor {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DefineDescriptor> for Box<dyn Step> {
|
||||
fn from(s: DefineDescriptor) -> Box<dyn Step> {
|
||||
Box::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DefineBitcoind {
|
||||
cookie_path: form::Value<String>,
|
||||
address: form::Value<String>,
|
||||
}
|
||||
|
||||
fn bitcoind_default_cookie_path(network: &bitcoin::Network) -> Option<String> {
|
||||
#[cfg(target_os = "linux")]
|
||||
let configs_dir = dirs::home_dir();
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let configs_dir = dirs::config_dir();
|
||||
|
||||
if let Some(mut path) = configs_dir {
|
||||
#[cfg(target_os = "linux")]
|
||||
path.push(".bitcoin");
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
path.push("Bitcoin");
|
||||
|
||||
match network {
|
||||
bitcoin::Network::Bitcoin => {
|
||||
path.push(".cookie");
|
||||
}
|
||||
bitcoin::Network::Testnet => {
|
||||
path.push("testnet3/.cookie");
|
||||
}
|
||||
bitcoin::Network::Regtest => {
|
||||
path.push("regtest/.cookie");
|
||||
}
|
||||
bitcoin::Network::Signet => {
|
||||
path.push("signet/.cookie");
|
||||
}
|
||||
}
|
||||
|
||||
return path.to_str().map(|s| s.to_string());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn bitcoind_default_address(network: &bitcoin::Network) -> String {
|
||||
match network {
|
||||
bitcoin::Network::Bitcoin => "127.0.0.1:8332".to_string(),
|
||||
bitcoin::Network::Testnet => "127.0.0.1:18332".to_string(),
|
||||
bitcoin::Network::Regtest => "127.0.0.1:18443".to_string(),
|
||||
bitcoin::Network::Signet => "127.0.0.1:38332".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
impl DefineBitcoind {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
cookie_path: form::Value::default(),
|
||||
address: form::Value::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Step for DefineBitcoind {
|
||||
fn load_context(&mut self, ctx: &Context) {
|
||||
if self.cookie_path.value.is_empty() {
|
||||
self.cookie_path.value = bitcoind_default_cookie_path(&ctx.network).unwrap_or_default()
|
||||
}
|
||||
if self.address.value.is_empty() {
|
||||
self.address.value = bitcoind_default_address(&ctx.network);
|
||||
}
|
||||
}
|
||||
fn update(&mut self, message: Message) {
|
||||
if let Message::DefineBitcoind(msg) = message {
|
||||
match msg {
|
||||
message::DefineBitcoind::AddressEdited(address) => {
|
||||
self.address.value = address;
|
||||
self.address.valid = true;
|
||||
}
|
||||
message::DefineBitcoind::CookiePathEdited(path) => {
|
||||
self.cookie_path.value = path;
|
||||
self.address.valid = true;
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
fn apply(&mut self, _ctx: &mut Context, config: &mut config::Config) -> bool {
|
||||
match (
|
||||
PathBuf::from_str(&self.cookie_path.value),
|
||||
std::net::SocketAddr::from_str(&self.address.value),
|
||||
) {
|
||||
(Err(_), Ok(_)) => {
|
||||
self.cookie_path.valid = false;
|
||||
false
|
||||
}
|
||||
(Ok(_), Err(_)) => {
|
||||
self.address.valid = false;
|
||||
false
|
||||
}
|
||||
(Err(_), Err(_)) => {
|
||||
self.cookie_path.valid = false;
|
||||
self.address.valid = false;
|
||||
false
|
||||
}
|
||||
(Ok(path), Ok(addr)) => {
|
||||
config.bitcoind_config.cookie_path = path;
|
||||
config.bitcoind_config.addr = addr;
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
view::define_bitcoin(&self.address, &self.cookie_path)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DefineBitcoind {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DefineBitcoind> for Box<dyn Step> {
|
||||
fn from(s: DefineBitcoind) -> Box<dyn Step> {
|
||||
Box::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Final {
|
||||
generating: bool,
|
||||
warning: Option<String>,
|
||||
config_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Final {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
generating: false,
|
||||
warning: None,
|
||||
config_path: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Step for Final {
|
||||
fn update(&mut self, message: Message) {
|
||||
match message {
|
||||
Message::Installed(res) => {
|
||||
self.generating = false;
|
||||
match res {
|
||||
Err(e) => {
|
||||
self.config_path = None;
|
||||
self.warning = Some(e.to_string());
|
||||
}
|
||||
Ok(path) => self.config_path = Some(path),
|
||||
}
|
||||
}
|
||||
Message::Install => {
|
||||
self.generating = true;
|
||||
self.config_path = None;
|
||||
self.warning = None;
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
view::install(
|
||||
self.generating,
|
||||
self.config_path.as_ref(),
|
||||
self.warning.as_ref(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Final {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Final> for Box<dyn Step> {
|
||||
fn from(s: Final) -> Box<dyn Step> {
|
||||
Box::new(s)
|
||||
}
|
||||
}
|
||||
247
gui/src/installer/view.rs
Normal file
247
gui/src/installer/view.rs
Normal file
@ -0,0 +1,247 @@
|
||||
use iced::pure::{column, container, pick_list, row, scrollable, Element};
|
||||
use iced::{Alignment, Length};
|
||||
|
||||
use crate::ui::{
|
||||
component::{
|
||||
button, form,
|
||||
text::{text, Text},
|
||||
},
|
||||
util::Collection,
|
||||
};
|
||||
|
||||
use crate::installer::message::{self, Message};
|
||||
|
||||
const NETWORKS: [bitcoin::Network; 4] = [
|
||||
bitcoin::Network::Bitcoin,
|
||||
bitcoin::Network::Testnet,
|
||||
bitcoin::Network::Signet,
|
||||
bitcoin::Network::Regtest,
|
||||
];
|
||||
|
||||
pub fn welcome(network: &bitcoin::Network) -> Element<Message> {
|
||||
container(container(
|
||||
column()
|
||||
.push(container(
|
||||
pick_list(&NETWORKS[..], Some(*network), message::Message::Network).padding(10),
|
||||
))
|
||||
.push(
|
||||
button::primary(None, "Install")
|
||||
.on_press(Message::Next)
|
||||
.width(Length::Units(200)),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.padding(100)
|
||||
.spacing(50)
|
||||
.align_items(Alignment::Center),
|
||||
))
|
||||
.center_y()
|
||||
.center_x()
|
||||
.height(Length::Fill)
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn define_descriptor<'a>(
|
||||
imported_descriptor: &form::Value<String>,
|
||||
user_xpub: &form::Value<String>,
|
||||
heir_xpub: &form::Value<String>,
|
||||
sequence: &form::Value<String>,
|
||||
error: Option<&String>,
|
||||
) -> Element<'a, Message> {
|
||||
let col_descriptor = column()
|
||||
.push(text("Descriptor:").bold())
|
||||
.push(
|
||||
form::Form::new("Descriptor", imported_descriptor, |msg| {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::ImportDescriptor(msg))
|
||||
})
|
||||
.warning("Please enter correct descriptor")
|
||||
.size(20)
|
||||
.padding(10),
|
||||
)
|
||||
.spacing(10);
|
||||
|
||||
let col_user_xpub = column()
|
||||
.push(text("Your xpub:").bold())
|
||||
.push(
|
||||
form::Form::new("Xpub", user_xpub, |msg| {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::UserXpubEdited(msg))
|
||||
})
|
||||
.warning("Please enter correct xpub")
|
||||
.size(20)
|
||||
.padding(10),
|
||||
)
|
||||
.spacing(10);
|
||||
|
||||
let col_heir_xpub = column()
|
||||
.push(text("Heir xpub:").bold())
|
||||
.push(
|
||||
form::Form::new("Xpub", heir_xpub, |msg| {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::HeirXpubEdited(msg))
|
||||
})
|
||||
.warning("Please enter correct xpub")
|
||||
.size(20)
|
||||
.padding(10),
|
||||
)
|
||||
.spacing(10);
|
||||
|
||||
let col_sequence = column()
|
||||
.push(text("Number of block").bold())
|
||||
.push(
|
||||
form::Form::new("Number of block", sequence, |msg| {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::SequenceEdited(msg))
|
||||
})
|
||||
.warning("Please enter correct block number")
|
||||
.size(20)
|
||||
.padding(10),
|
||||
)
|
||||
.spacing(10);
|
||||
|
||||
layout(
|
||||
column()
|
||||
.push(text("Create the descriptor").bold().size(50))
|
||||
.push(
|
||||
column()
|
||||
.push(col_user_xpub)
|
||||
.push(
|
||||
row()
|
||||
.push(col_sequence.width(Length::FillPortion(1)))
|
||||
.push(col_heir_xpub.width(Length::FillPortion(4)))
|
||||
.spacing(20),
|
||||
)
|
||||
.spacing(20),
|
||||
)
|
||||
.push(text("or import it").bold().size(25))
|
||||
.push(col_descriptor)
|
||||
.push(
|
||||
if !imported_descriptor.value.is_empty()
|
||||
&& (!user_xpub.value.is_empty()
|
||||
|| !heir_xpub.value.is_empty()
|
||||
|| !sequence.value.is_empty())
|
||||
{
|
||||
button::primary(None, "Next").width(Length::Units(200))
|
||||
} else {
|
||||
button::primary(None, "Next")
|
||||
.width(Length::Units(200))
|
||||
.on_press(Message::Next)
|
||||
},
|
||||
)
|
||||
.push_maybe(error.map(|e| text(e).size(15)))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.padding(100)
|
||||
.spacing(50)
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn define_bitcoin<'a>(
|
||||
address: &form::Value<String>,
|
||||
cookie_path: &form::Value<String>,
|
||||
) -> Element<'a, Message> {
|
||||
let col_address = column()
|
||||
.push(text("Address:").bold())
|
||||
.push(
|
||||
form::Form::new("Address", address, |msg| {
|
||||
Message::DefineBitcoind(message::DefineBitcoind::AddressEdited(msg))
|
||||
})
|
||||
.warning("Please enter correct address")
|
||||
.size(20)
|
||||
.padding(10),
|
||||
)
|
||||
.spacing(10);
|
||||
|
||||
let col_cookie = column()
|
||||
.push(text("Cookie path:").bold())
|
||||
.push(
|
||||
form::Form::new("Cookie path", cookie_path, |msg| {
|
||||
Message::DefineBitcoind(message::DefineBitcoind::CookiePathEdited(msg))
|
||||
})
|
||||
.warning("Please enter correct path")
|
||||
.size(20)
|
||||
.padding(10),
|
||||
)
|
||||
.spacing(10);
|
||||
|
||||
layout(
|
||||
column()
|
||||
.push(
|
||||
text("Set up connection to the Bitcoin full node")
|
||||
.bold()
|
||||
.size(50),
|
||||
)
|
||||
.push(col_address)
|
||||
.push(col_cookie)
|
||||
.push(
|
||||
button::primary(None, "Next")
|
||||
.on_press(Message::Next)
|
||||
.width(Length::Units(200)),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.padding(100)
|
||||
.spacing(50)
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn install<'a>(
|
||||
generating: bool,
|
||||
config_path: Option<&std::path::PathBuf>,
|
||||
warning: Option<&String>,
|
||||
) -> Element<'a, Message> {
|
||||
let mut col = column()
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.padding(100)
|
||||
.spacing(50)
|
||||
.align_items(Alignment::Center);
|
||||
|
||||
if let Some(error) = warning {
|
||||
col = col.push(text(error));
|
||||
}
|
||||
|
||||
if generating {
|
||||
col = col.push(button::primary(None, "Installing ...").width(Length::Units(200)))
|
||||
} else if let Some(path) = config_path {
|
||||
col = col.push(
|
||||
container(
|
||||
column()
|
||||
.push(container(text("Installed !")))
|
||||
.push(container(
|
||||
button::primary(None, "Start")
|
||||
.on_press(Message::Exit(path.clone()))
|
||||
.width(Length::Units(200)),
|
||||
))
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(20),
|
||||
)
|
||||
.padding(50)
|
||||
.width(Length::Fill)
|
||||
.center_x(),
|
||||
);
|
||||
} else {
|
||||
col = col.push(
|
||||
button::primary(None, "Finalize installation")
|
||||
.on_press(Message::Install)
|
||||
.width(Length::Units(200)),
|
||||
);
|
||||
}
|
||||
|
||||
layout(col)
|
||||
}
|
||||
|
||||
fn layout<'a>(content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
|
||||
container(scrollable(
|
||||
column()
|
||||
.push(
|
||||
container(button::transparent(None, "< Previous").on_press(Message::Previous))
|
||||
.padding(5),
|
||||
)
|
||||
.push(container(content).width(Length::Fill).center_x()),
|
||||
))
|
||||
.center_x()
|
||||
.height(Length::Fill)
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
6
gui/src/lib.rs
Normal file
6
gui/src/lib.rs
Normal file
@ -0,0 +1,6 @@
|
||||
pub mod app;
|
||||
pub mod conversion;
|
||||
pub mod daemon;
|
||||
pub mod installer;
|
||||
pub mod loader;
|
||||
pub mod ui;
|
||||
281
gui/src/loader.rs
Normal file
281
gui/src/loader.rs
Normal file
@ -0,0 +1,281 @@
|
||||
use std::convert::From;
|
||||
use std::io::ErrorKind;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use iced::pure::{column, text, Element};
|
||||
use iced::{Alignment, Command, Subscription};
|
||||
use iced_native::{window, Event};
|
||||
use log::{debug, info};
|
||||
|
||||
use minisafe::config::{Config, ConfigError};
|
||||
|
||||
use crate::{
|
||||
app::config::{default_datadir, Config as GUIConfig},
|
||||
daemon::{client, embedded::EmbeddedDaemon, model::*, Daemon, DaemonError},
|
||||
};
|
||||
|
||||
type Minisafed = client::Minisafed<client::jsonrpc::JsonRPCClient>;
|
||||
|
||||
pub struct Loader {
|
||||
pub gui_config: GUIConfig,
|
||||
pub daemon_config: Config,
|
||||
pub daemon_started: bool,
|
||||
|
||||
should_exit: bool,
|
||||
step: Step,
|
||||
}
|
||||
|
||||
enum Step {
|
||||
Connecting,
|
||||
StartingDaemon,
|
||||
Syncing {
|
||||
daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
progress: f64,
|
||||
},
|
||||
Error(Box<Error>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Message {
|
||||
Event(iced_native::Event),
|
||||
Syncing(Result<GetInfoResult, DaemonError>),
|
||||
Synced(GetInfoResult, Arc<dyn Daemon + Sync + Send>),
|
||||
Started(Result<Arc<dyn Daemon + Sync + Send>, Error>),
|
||||
Loaded(Result<Arc<dyn Daemon + Sync + Send>, Error>),
|
||||
DaemonStarted(EmbeddedDaemon),
|
||||
Failure(DaemonError),
|
||||
}
|
||||
|
||||
impl Loader {
|
||||
pub fn new(gui_config: GUIConfig, daemon_config: Config) -> (Self, Command<Message>) {
|
||||
let path = socket_path(
|
||||
&daemon_config.data_dir,
|
||||
daemon_config.bitcoin_config.network,
|
||||
)
|
||||
.unwrap();
|
||||
(
|
||||
Loader {
|
||||
daemon_config,
|
||||
gui_config,
|
||||
step: Step::Connecting,
|
||||
should_exit: false,
|
||||
daemon_started: false,
|
||||
},
|
||||
Command::perform(connect(path), Message::Loaded),
|
||||
)
|
||||
}
|
||||
|
||||
fn on_load(&mut self, res: Result<Arc<dyn Daemon + Sync + Send>, Error>) -> Command<Message> {
|
||||
match res {
|
||||
Ok(daemon) => {
|
||||
self.step = Step::Syncing {
|
||||
daemon: daemon.clone(),
|
||||
progress: 0.0,
|
||||
};
|
||||
return Command::perform(sync(daemon, false), Message::Syncing);
|
||||
}
|
||||
Err(e) => match e {
|
||||
Error::Config(_) => {
|
||||
self.step = Step::Error(Box::new(e));
|
||||
}
|
||||
Error::Daemon(DaemonError::Transport(Some(ErrorKind::ConnectionRefused), _))
|
||||
| Error::Daemon(DaemonError::Transport(Some(ErrorKind::NotFound), _)) => {
|
||||
self.step = Step::StartingDaemon;
|
||||
self.daemon_started = true;
|
||||
return Command::perform(
|
||||
start_daemon(self.gui_config.minisafed_config_path.clone()),
|
||||
Message::Started,
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
self.step = Step::Error(Box::new(e));
|
||||
}
|
||||
},
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn on_start(&mut self, res: Result<Arc<dyn Daemon + Sync + Send>, Error>) -> Command<Message> {
|
||||
match res {
|
||||
Ok(minisafed) => {
|
||||
self.step = Step::Syncing {
|
||||
daemon: minisafed.clone(),
|
||||
progress: 0.0,
|
||||
};
|
||||
Command::perform(sync(minisafed, false), Message::Syncing)
|
||||
}
|
||||
Err(e) => {
|
||||
self.step = Step::Error(Box::new(e));
|
||||
Command::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_sync(&mut self, res: Result<GetInfoResult, DaemonError>) -> Command<Message> {
|
||||
match &mut self.step {
|
||||
Step::Syncing {
|
||||
daemon, progress, ..
|
||||
} => {
|
||||
match res {
|
||||
Ok(info) => {
|
||||
if (info.sync - 1.0_f64).abs() < f64::EPSILON {
|
||||
let daemon = daemon.clone();
|
||||
return Command::perform(async move { (info, daemon) }, |res| {
|
||||
Message::Synced(res.0, res.1)
|
||||
});
|
||||
} else {
|
||||
*progress = info.sync
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
self.step = Step::Error(Box::new(e.into()));
|
||||
return Command::none();
|
||||
}
|
||||
};
|
||||
Command::perform(sync(daemon.clone(), true), Message::Syncing)
|
||||
}
|
||||
_ => Command::none(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) {
|
||||
log::info!("Close requested");
|
||||
if let Step::Syncing { daemon, .. } = &mut self.step {
|
||||
if !daemon.is_external() {
|
||||
log::info!("Stopping internal daemon...");
|
||||
if let Some(d) = Arc::get_mut(daemon) {
|
||||
d.stop().expect("Daemon is internal");
|
||||
log::info!("Internal daemon stopped");
|
||||
self.should_exit = true;
|
||||
}
|
||||
} else {
|
||||
self.should_exit = true;
|
||||
}
|
||||
} else {
|
||||
self.should_exit = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, message: Message) -> Command<Message> {
|
||||
match message {
|
||||
Message::Started(res) => self.on_start(res),
|
||||
Message::Loaded(res) => self.on_load(res),
|
||||
Message::Syncing(res) => self.on_sync(res),
|
||||
Message::Failure(_) => {
|
||||
self.daemon_started = false;
|
||||
Command::none()
|
||||
}
|
||||
Message::Event(Event::Window(window::Event::CloseRequested)) => {
|
||||
self.stop();
|
||||
Command::none()
|
||||
}
|
||||
_ => Command::none(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscription(&self) -> Subscription<Message> {
|
||||
iced_native::subscription::events().map(Message::Event)
|
||||
}
|
||||
|
||||
pub fn should_exit(&self) -> bool {
|
||||
self.should_exit
|
||||
}
|
||||
|
||||
pub fn view(&self) -> Element<Message> {
|
||||
match &self.step {
|
||||
Step::StartingDaemon => cover(text("Starting daemon...")),
|
||||
Step::Connecting => cover(text("Connecting to daemon...")),
|
||||
Step::Syncing { progress, .. } => cover(text(&format!("Syncing... {}%", progress))),
|
||||
Step::Error(error) => cover(text(&format!("Error: {}", error))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cover<'a, T: 'a, C: Into<Element<'a, T>>>(content: C) -> Element<'a, T> {
|
||||
column()
|
||||
.push(content)
|
||||
.width(iced::Length::Fill)
|
||||
.height(iced::Length::Fill)
|
||||
.padding(50)
|
||||
.spacing(50)
|
||||
.align_items(Alignment::Center)
|
||||
.into()
|
||||
}
|
||||
|
||||
async fn connect(socket_path: PathBuf) -> Result<Arc<dyn Daemon + Sync + Send>, Error> {
|
||||
let client = client::jsonrpc::JsonRPCClient::new(socket_path);
|
||||
let minisafed = Minisafed::new(client);
|
||||
|
||||
debug!("Searching for external daemon");
|
||||
minisafed.get_info()?;
|
||||
info!("Connected to external daemon");
|
||||
|
||||
Ok(Arc::new(minisafed))
|
||||
}
|
||||
|
||||
// Daemon can start only if a config path is given.
|
||||
pub async fn start_daemon(config_path: PathBuf) -> Result<Arc<dyn Daemon + Sync + Send>, Error> {
|
||||
debug!("starting minisafe daemon");
|
||||
|
||||
let config = Config::from_file(Some(config_path))
|
||||
.map_err(|e| DaemonError::Start(format!("Error parsing config: {}", e)))?;
|
||||
|
||||
let mut daemon = EmbeddedDaemon::default();
|
||||
daemon.start(config)?;
|
||||
|
||||
Ok(Arc::new(daemon))
|
||||
}
|
||||
|
||||
async fn sync(
|
||||
minisafed: Arc<dyn Daemon + Sync + Send>,
|
||||
sleep: bool,
|
||||
) -> Result<GetInfoResult, DaemonError> {
|
||||
if sleep {
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
}
|
||||
minisafed.get_info()
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Config(ConfigError),
|
||||
Daemon(DaemonError),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Config(e) => write!(f, "Config error: {}", e),
|
||||
Self::Daemon(e) => write!(f, "RevaultD error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConfigError> for Error {
|
||||
fn from(error: ConfigError) -> Self {
|
||||
Error::Config(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DaemonError> for Error {
|
||||
fn from(error: DaemonError) -> Self {
|
||||
Error::Daemon(error)
|
||||
}
|
||||
}
|
||||
|
||||
/// default minisafed socket path is .minisafe/bitcoin/minisafed_rpc
|
||||
fn socket_path(
|
||||
datadir: &Option<PathBuf>,
|
||||
network: bitcoin::Network,
|
||||
) -> Result<PathBuf, ConfigError> {
|
||||
let mut path = if let Some(ref datadir) = datadir {
|
||||
datadir.clone()
|
||||
} else {
|
||||
default_datadir().map_err(|_| ConfigError::DatadirNotFound)?
|
||||
};
|
||||
path.push(network.to_string());
|
||||
path.push("minisafed_rpc");
|
||||
Ok(path)
|
||||
}
|
||||
362
gui/src/main.rs
Normal file
362
gui/src/main.rs
Normal file
@ -0,0 +1,362 @@
|
||||
use std::{error::Error, path::PathBuf, str::FromStr};
|
||||
|
||||
use iced::pure::{Application, Element};
|
||||
use iced::{executor, Command, Settings, Subscription};
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
|
||||
use minisafe::config::Config as DaemonConfig;
|
||||
|
||||
use minisafe_gui::{
|
||||
app::{
|
||||
self,
|
||||
config::{default_datadir, ConfigError},
|
||||
context::{ConfigContext, Context},
|
||||
menu::Menu,
|
||||
App,
|
||||
},
|
||||
conversion::Converter,
|
||||
installer::{self, Installer},
|
||||
loader::{self, Loader},
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum Arg {
|
||||
ConfigPath(PathBuf),
|
||||
DatadirPath(PathBuf),
|
||||
Network(bitcoin::Network),
|
||||
}
|
||||
|
||||
fn parse_args(args: Vec<String>) -> Result<Vec<Arg>, Box<dyn Error>> {
|
||||
let mut res = Vec::new();
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
if arg == "--conf" {
|
||||
if let Some(a) = args.get(i + 1) {
|
||||
res.push(Arg::ConfigPath(PathBuf::from(a)));
|
||||
} else {
|
||||
return Err("missing arg to --conf".into());
|
||||
}
|
||||
} else if arg == "--datadir" {
|
||||
if let Some(a) = args.get(i + 1) {
|
||||
res.push(Arg::DatadirPath(PathBuf::from(a)));
|
||||
} else {
|
||||
return Err("missing arg to --datadir".into());
|
||||
}
|
||||
} else if arg.contains("--") {
|
||||
let network = bitcoin::Network::from_str(args[i].trim_start_matches("--"))?;
|
||||
res.push(Arg::Network(network));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn log_level_from_config(config: &app::Config) -> Result<log::LevelFilter, Box<dyn Error>> {
|
||||
if let Some(level) = &config.log_level {
|
||||
match level.as_ref() {
|
||||
"info" => Ok(log::LevelFilter::Info),
|
||||
"debug" => Ok(log::LevelFilter::Debug),
|
||||
"trace" => Ok(log::LevelFilter::Trace),
|
||||
_ => Err(format!("Unknown loglevel '{:?}'.", level).into()),
|
||||
}
|
||||
} else if let Some(true) = config.debug {
|
||||
Ok(log::LevelFilter::Debug)
|
||||
} else {
|
||||
Ok(log::LevelFilter::Info)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GUI {
|
||||
state: State,
|
||||
}
|
||||
|
||||
enum State {
|
||||
Installer(Installer),
|
||||
Loader(Loader),
|
||||
App(App),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Message {
|
||||
CtrlC,
|
||||
Install(Box<installer::Message>),
|
||||
Load(Box<loader::Message>),
|
||||
Run(Box<app::Message>),
|
||||
}
|
||||
|
||||
async fn ctrl_c() -> Result<(), ()> {
|
||||
if let Err(e) = tokio::signal::ctrl_c().await {
|
||||
log::error!("{}", e);
|
||||
};
|
||||
log::info!("Signal received, exiting");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Application for GUI {
|
||||
type Executor = executor::Default;
|
||||
type Message = Message;
|
||||
type Flags = Config;
|
||||
|
||||
fn title(&self) -> String {
|
||||
match self.state {
|
||||
State::Installer(_) => String::from("Revault Installer"),
|
||||
State::App(_) => String::from("Revault GUI"),
|
||||
State::Loader(..) => String::from("Revault"),
|
||||
}
|
||||
}
|
||||
|
||||
fn new(config: Config) -> (GUI, Command<Self::Message>) {
|
||||
match config {
|
||||
Config::Install(config_path, network) => {
|
||||
let (install, command) = Installer::new(config_path, network);
|
||||
(
|
||||
Self {
|
||||
state: State::Installer(install),
|
||||
},
|
||||
Command::batch(vec![
|
||||
command.map(|msg| Message::Install(Box::new(msg))),
|
||||
Command::perform(ctrl_c(), |_| Message::CtrlC),
|
||||
]),
|
||||
)
|
||||
}
|
||||
Config::Run(cfg) => {
|
||||
let daemon_cfg =
|
||||
DaemonConfig::from_file(Some(cfg.minisafed_config_path.clone())).unwrap();
|
||||
let (loader, command) = Loader::new(cfg, daemon_cfg);
|
||||
(
|
||||
Self {
|
||||
state: State::Loader(loader),
|
||||
},
|
||||
Command::batch(vec![
|
||||
command.map(|msg| Message::Load(Box::new(msg))),
|
||||
Command::perform(ctrl_c(), |_| Message::CtrlC),
|
||||
]),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
|
||||
match (&mut self.state, message) {
|
||||
(State::Installer(i), Message::CtrlC) => {
|
||||
i.stop();
|
||||
Command::none()
|
||||
}
|
||||
(State::Loader(i), Message::CtrlC) => {
|
||||
i.stop();
|
||||
Command::none()
|
||||
}
|
||||
(State::App(i), Message::CtrlC) => {
|
||||
i.stop();
|
||||
Command::none()
|
||||
}
|
||||
(State::Installer(i), Message::Install(msg)) => {
|
||||
if let installer::Message::Exit(path) = *msg {
|
||||
let cfg = app::Config::from_file(&path).unwrap();
|
||||
let daemon_cfg =
|
||||
DaemonConfig::from_file(Some(cfg.minisafed_config_path.clone())).unwrap();
|
||||
let (loader, command) = Loader::new(cfg, daemon_cfg);
|
||||
self.state = State::Loader(loader);
|
||||
command.map(|msg| Message::Load(Box::new(msg)))
|
||||
} else {
|
||||
i.update(*msg).map(|msg| Message::Install(Box::new(msg)))
|
||||
}
|
||||
}
|
||||
(State::Loader(loader), Message::Load(msg)) => {
|
||||
if let loader::Message::Synced(_info, minisafed) = *msg {
|
||||
let config = ConfigContext {
|
||||
gui: loader.gui_config.clone(),
|
||||
daemon: loader.daemon_config.clone(),
|
||||
};
|
||||
|
||||
let converter = Converter::new(config.daemon.bitcoin_config.network);
|
||||
|
||||
let context = Context::new(config, minisafed, converter, Menu::Home);
|
||||
|
||||
let (app, command) = App::new(context);
|
||||
self.state = State::App(app);
|
||||
command.map(|msg| Message::Run(Box::new(msg)))
|
||||
} else {
|
||||
loader.update(*msg).map(|msg| Message::Load(Box::new(msg)))
|
||||
}
|
||||
}
|
||||
(State::App(i), Message::Run(msg)) => {
|
||||
i.update(*msg).map(|msg| Message::Run(Box::new(msg)))
|
||||
}
|
||||
_ => Command::none(),
|
||||
}
|
||||
}
|
||||
|
||||
fn should_exit(&self) -> bool {
|
||||
match &self.state {
|
||||
State::Installer(v) => v.should_exit(),
|
||||
State::Loader(v) => v.should_exit(),
|
||||
State::App(v) => v.should_exit(),
|
||||
}
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Self::Message> {
|
||||
match &self.state {
|
||||
State::Installer(v) => v.subscription().map(|msg| Message::Install(Box::new(msg))),
|
||||
State::Loader(v) => v.subscription().map(|msg| Message::Load(Box::new(msg))),
|
||||
State::App(v) => v.subscription().map(|msg| Message::Run(Box::new(msg))),
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<Self::Message> {
|
||||
match &self.state {
|
||||
State::Installer(v) => v.view().map(|msg| Message::Install(Box::new(msg))),
|
||||
State::App(v) => v.view().map(|msg| Message::Run(Box::new(msg))),
|
||||
State::Loader(v) => v.view().map(|msg| Message::Load(Box::new(msg))),
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_factor(&self) -> f64 {
|
||||
1.0
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Config {
|
||||
Run(app::Config),
|
||||
Install(PathBuf, bitcoin::Network),
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new(datadir_path: PathBuf, network: bitcoin::Network) -> Result<Self, Box<dyn Error>> {
|
||||
let mut path = datadir_path.clone();
|
||||
path.push(network.to_string());
|
||||
path.push(app::config::DEFAULT_FILE_NAME);
|
||||
match app::Config::from_file(&path) {
|
||||
Ok(cfg) => Ok(Config::Run(cfg)),
|
||||
Err(ConfigError::NotFound) => Ok(Config::Install(datadir_path, network)),
|
||||
Err(e) => Err(format!("Failed to read configuration file: {}", e).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let args = parse_args(std::env::args().collect())?;
|
||||
let config = match args.as_slice() {
|
||||
[] => {
|
||||
let datadir_path = default_datadir().unwrap();
|
||||
Config::new(datadir_path, bitcoin::Network::Bitcoin)
|
||||
}
|
||||
[Arg::Network(network)] => {
|
||||
let datadir_path = default_datadir().unwrap();
|
||||
Config::new(datadir_path, *network)
|
||||
}
|
||||
[Arg::ConfigPath(path)] => Ok(Config::Run(app::Config::from_file(path)?)),
|
||||
[Arg::DatadirPath(datadir_path)] => {
|
||||
Config::new(datadir_path.clone(), bitcoin::Network::Bitcoin)
|
||||
}
|
||||
[Arg::DatadirPath(datadir_path), Arg::Network(network)]
|
||||
| [Arg::Network(network), Arg::DatadirPath(datadir_path)] => {
|
||||
Config::new(datadir_path.clone(), *network)
|
||||
}
|
||||
_ => {
|
||||
return Err("Unknown args combination".into());
|
||||
}
|
||||
}?;
|
||||
|
||||
let level = if let Config::Run(cfg) = &config {
|
||||
log_level_from_config(cfg)?
|
||||
} else {
|
||||
log::LevelFilter::Info
|
||||
};
|
||||
setup_logger(level)?;
|
||||
|
||||
let mut settings = Settings::with_flags(config);
|
||||
settings.exit_on_close_request = false;
|
||||
|
||||
if let Err(e) = GUI::run(settings) {
|
||||
return Err(format!("Failed to launch UI: {}", e).into());
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// This creates the log file automagically if it doesn't exist, and logs on stdout
|
||||
// if None is given
|
||||
pub fn setup_logger(log_level: log::LevelFilter) -> Result<(), fern::InitError> {
|
||||
let dispatcher = fern::Dispatch::new()
|
||||
.format(|out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"[{}][{}][{}] {}",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_else(|e| {
|
||||
println!("Can't get time since epoch: '{}'. Using a dummy value.", e);
|
||||
std::time::Duration::from_secs(0)
|
||||
})
|
||||
.as_secs(),
|
||||
record.target(),
|
||||
record.level(),
|
||||
message
|
||||
))
|
||||
})
|
||||
.level(log_level)
|
||||
.level_for("iced_wgpu", log::LevelFilter::Off)
|
||||
.level_for("wgpu_core", log::LevelFilter::Off)
|
||||
.level_for("wgpu_hal", log::LevelFilter::Off)
|
||||
.level_for("gfx_backend_vulkan", log::LevelFilter::Off)
|
||||
.level_for("naga", log::LevelFilter::Off)
|
||||
.level_for("mio", log::LevelFilter::Off);
|
||||
|
||||
dispatcher.chain(std::io::stdout()).apply()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_args() {
|
||||
assert_eq!(true, parse_args(vec!["--meth".into()]).is_err());
|
||||
assert_eq!(true, parse_args(vec!["--datadir".into()]).is_err());
|
||||
assert_eq!(true, parse_args(vec!["--conf".into()]).is_err());
|
||||
assert_eq!(
|
||||
Some(vec![
|
||||
Arg::DatadirPath(PathBuf::from(".")),
|
||||
Arg::ConfigPath(PathBuf::from("hello.toml")),
|
||||
]),
|
||||
parse_args(
|
||||
"--datadir . --conf hello.toml"
|
||||
.split(" ")
|
||||
.map(|a| a.to_string())
|
||||
.collect()
|
||||
)
|
||||
.ok()
|
||||
);
|
||||
assert_eq!(
|
||||
Some(vec![Arg::Network(bitcoin::Network::Regtest)]),
|
||||
parse_args(vec!["--regtest".into()]).ok()
|
||||
);
|
||||
assert_eq!(
|
||||
Some(vec![
|
||||
Arg::DatadirPath(PathBuf::from("hello")),
|
||||
Arg::Network(bitcoin::Network::Testnet)
|
||||
]),
|
||||
parse_args(
|
||||
"--datadir hello --testnet"
|
||||
.split(" ")
|
||||
.map(|a| a.to_string())
|
||||
.collect()
|
||||
)
|
||||
.ok()
|
||||
);
|
||||
assert_eq!(
|
||||
Some(vec![
|
||||
Arg::Network(bitcoin::Network::Testnet),
|
||||
Arg::DatadirPath(PathBuf::from("hello"))
|
||||
]),
|
||||
parse_args(
|
||||
"--testnet --datadir hello"
|
||||
.split(" ")
|
||||
.map(|a| a.to_string())
|
||||
.collect()
|
||||
)
|
||||
.ok()
|
||||
);
|
||||
}
|
||||
}
|
||||
75
gui/src/ui/color.rs
Normal file
75
gui/src/ui/color.rs
Normal file
@ -0,0 +1,75 @@
|
||||
use iced::Color;
|
||||
|
||||
pub const BACKGROUND: Color = Color::from_rgb(
|
||||
0xF6 as f32 / 255.0,
|
||||
0xF7 as f32 / 255.0,
|
||||
0xF8 as f32 / 255.0,
|
||||
);
|
||||
|
||||
pub const FOREGROUND: Color = Color::WHITE;
|
||||
|
||||
pub const PRIMARY: Color = Color::BLACK;
|
||||
|
||||
pub const SECONDARY: Color = DARK_GREY;
|
||||
|
||||
pub const SUCCESS: Color = Color::from_rgb(
|
||||
0x29 as f32 / 255.0,
|
||||
0xBC as f32 / 255.0,
|
||||
0x97 as f32 / 255.0,
|
||||
);
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub const SUCCESS_LIGHT: Color = Color::from_rgba(
|
||||
0x29 as f32 / 255.0,
|
||||
0xBC as f32 / 255.0,
|
||||
0x97 as f32 / 255.0,
|
||||
0.5f32,
|
||||
);
|
||||
|
||||
pub const ALERT: Color = Color::from_rgb(
|
||||
0xF0 as f32 / 255.0,
|
||||
0x43 as f32 / 255.0,
|
||||
0x59 as f32 / 255.0,
|
||||
);
|
||||
|
||||
pub const ALERT_LIGHT: Color = Color::from_rgba(
|
||||
0xF0 as f32 / 255.0,
|
||||
0x43 as f32 / 255.0,
|
||||
0x59 as f32 / 255.0,
|
||||
0.5f32,
|
||||
);
|
||||
|
||||
pub const WARNING: Color =
|
||||
Color::from_rgb(0xFF as f32 / 255.0, 0xa7 as f32 / 255.0, 0x0 as f32 / 255.0);
|
||||
|
||||
pub const WARNING_LIGHT: Color = Color::from_rgba(
|
||||
0xFF as f32 / 255.0,
|
||||
0xa7 as f32 / 255.0,
|
||||
0x0 as f32 / 255.0,
|
||||
0.5f32,
|
||||
);
|
||||
|
||||
pub const CANCEL: Color = Color::from_rgb(
|
||||
0x34 as f32 / 255.0,
|
||||
0x37 as f32 / 255.0,
|
||||
0x3D as f32 / 255.0,
|
||||
);
|
||||
|
||||
pub const INFO: Color = Color::from_rgb(
|
||||
0x2A as f32 / 255.0,
|
||||
0x98 as f32 / 255.0,
|
||||
0xBD as f32 / 255.0,
|
||||
);
|
||||
|
||||
pub const INFO_LIGHT: Color = Color::from_rgba(
|
||||
0x2A as f32 / 255.0,
|
||||
0x98 as f32 / 255.0,
|
||||
0xBD as f32 / 255.0,
|
||||
0.5f32,
|
||||
);
|
||||
|
||||
pub const DARK_GREY: Color = Color::from_rgb(
|
||||
0x8c as f32 / 255.0,
|
||||
0x97 as f32 / 255.0,
|
||||
0xa6 as f32 / 255.0,
|
||||
);
|
||||
62
gui/src/ui/component/button.rs
Normal file
62
gui/src/ui/component/button.rs
Normal file
@ -0,0 +1,62 @@
|
||||
use iced::pure::{
|
||||
container, row,
|
||||
widget::{button, Container},
|
||||
};
|
||||
use iced::{Alignment, Color, Length, Vector};
|
||||
|
||||
use super::text::text;
|
||||
use crate::ui::color;
|
||||
|
||||
pub fn primary<'a, T: 'a>(icon: Option<iced::Text>, t: &str) -> button::Button<'a, T> {
|
||||
button::Button::new(content(icon, t)).style(Style::Primary)
|
||||
}
|
||||
|
||||
pub fn transparent<'a, T: 'a>(icon: Option<iced::Text>, t: &str) -> button::Button<'a, T> {
|
||||
button::Button::new(content(icon, t)).style(Style::Transparent)
|
||||
}
|
||||
|
||||
fn content<'a, T: 'a>(icon: Option<iced::Text>, t: &str) -> Container<'a, T> {
|
||||
match icon {
|
||||
None => container(text(t)).width(Length::Fill).center_x().padding(5),
|
||||
Some(i) => container(
|
||||
row()
|
||||
.push(i)
|
||||
.push(text(t))
|
||||
.spacing(10)
|
||||
.width(iced::Length::Fill)
|
||||
.align_items(Alignment::Center),
|
||||
)
|
||||
.width(iced::Length::Fill)
|
||||
.center_x()
|
||||
.padding(5),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Style {
|
||||
Primary,
|
||||
Transparent,
|
||||
}
|
||||
|
||||
impl button::StyleSheet for Style {
|
||||
fn active(&self) -> button::Style {
|
||||
match self {
|
||||
Style::Primary => button::Style {
|
||||
shadow_offset: Vector::default(),
|
||||
background: color::PRIMARY.into(),
|
||||
border_radius: 10.0,
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
text_color: color::FOREGROUND,
|
||||
},
|
||||
Style::Transparent => button::Style {
|
||||
shadow_offset: Vector::default(),
|
||||
background: Color::TRANSPARENT.into(),
|
||||
border_radius: 10.0,
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
text_color: color::DARK_GREY,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
120
gui/src/ui/component/form.rs
Normal file
120
gui/src/ui/component/form.rs
Normal file
@ -0,0 +1,120 @@
|
||||
use iced::pure::{
|
||||
column, container,
|
||||
widget::text_input::{Style, StyleSheet, TextInput},
|
||||
Element,
|
||||
};
|
||||
use iced::Length;
|
||||
|
||||
use crate::ui::{color, component::text::text};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Value<T> {
|
||||
pub value: T,
|
||||
pub valid: bool,
|
||||
}
|
||||
|
||||
impl std::default::Default for Value<String> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
value: "".to_string(),
|
||||
valid: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Form<'a, Message> {
|
||||
input: TextInput<'a, Message>,
|
||||
warning: Option<&'a str>,
|
||||
valid: bool,
|
||||
}
|
||||
|
||||
impl<'a, Message: 'a> Form<'a, Message>
|
||||
where
|
||||
Message: Clone,
|
||||
{
|
||||
/// Creates a new [`Form`].
|
||||
///
|
||||
/// It expects:
|
||||
/// - a placeholder
|
||||
/// - the current value
|
||||
/// - a function that produces a message when the [`Form`] changes
|
||||
pub fn new<F>(placeholder: &str, value: &Value<String>, on_change: F) -> Self
|
||||
where
|
||||
F: 'static + Fn(String) -> Message,
|
||||
{
|
||||
Self {
|
||||
input: TextInput::new(placeholder, &value.value, on_change),
|
||||
warning: None,
|
||||
valid: value.valid,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the [`Form`] with a warning message
|
||||
pub fn warning(mut self, warning: &'a str) -> Self {
|
||||
self.warning = Some(warning);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the padding of the [`Form`].
|
||||
pub fn padding(mut self, units: u16) -> Self {
|
||||
self.input = self.input.padding(units);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Form`] with a text size
|
||||
pub fn size(mut self, size: u16) -> Self {
|
||||
self.input = self.input.size(size);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: 'a + Clone> From<Form<'a, Message>> for Element<'a, Message> {
|
||||
fn from(form: Form<'a, Message>) -> Element<'a, Message> {
|
||||
if !form.valid {
|
||||
if let Some(message) = form.warning {
|
||||
return container(
|
||||
column()
|
||||
.push(form.input.style(InvalidFormStyle))
|
||||
.push(text(message).color(color::ALERT))
|
||||
.width(Length::Fill)
|
||||
.spacing(5),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.into();
|
||||
}
|
||||
}
|
||||
|
||||
container(form.input).width(Length::Fill).into()
|
||||
}
|
||||
}
|
||||
|
||||
struct InvalidFormStyle;
|
||||
impl StyleSheet for InvalidFormStyle {
|
||||
fn active(&self) -> Style {
|
||||
Style {
|
||||
background: iced::Background::Color(color::FOREGROUND),
|
||||
border_radius: 5.0,
|
||||
border_width: 1.0,
|
||||
border_color: color::ALERT,
|
||||
}
|
||||
}
|
||||
|
||||
fn focused(&self) -> Style {
|
||||
Style {
|
||||
border_color: color::ALERT,
|
||||
..self.active()
|
||||
}
|
||||
}
|
||||
|
||||
fn placeholder_color(&self) -> iced::Color {
|
||||
iced::Color::from_rgb(0.7, 0.7, 0.7)
|
||||
}
|
||||
|
||||
fn value_color(&self) -> iced::Color {
|
||||
iced::Color::from_rgb(0.3, 0.3, 0.3)
|
||||
}
|
||||
|
||||
fn selection_color(&self) -> iced::Color {
|
||||
iced::Color::from_rgb(0.8, 0.8, 1.0)
|
||||
}
|
||||
}
|
||||
24
gui/src/ui/component/mod.rs
Normal file
24
gui/src/ui/component/mod.rs
Normal file
@ -0,0 +1,24 @@
|
||||
pub mod button;
|
||||
pub mod form;
|
||||
pub mod text;
|
||||
|
||||
use iced::pure::widget::{container, Column, Container};
|
||||
use iced::Length;
|
||||
|
||||
use crate::ui::color;
|
||||
|
||||
pub fn separation<'a, T: 'a>() -> Container<'a, T> {
|
||||
Container::new(Column::new().push(iced::Text::new(" ")))
|
||||
.style(SepStyle)
|
||||
.height(Length::Units(1))
|
||||
}
|
||||
|
||||
pub struct SepStyle;
|
||||
impl container::StyleSheet for SepStyle {
|
||||
fn style(&self) -> container::Style {
|
||||
container::Style {
|
||||
background: color::SECONDARY.into(),
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
46
gui/src/ui/component/notification.rs
Normal file
46
gui/src/ui/component/notification.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use crate::{color, icon};
|
||||
use iced::{container, tooltip, Container, Length, Row, Text, Tooltip};
|
||||
|
||||
pub fn warning<'a, T: 'a>(message: &str, error: &str) -> Container<'a, T> {
|
||||
Container::new(Container::new(
|
||||
Tooltip::new(
|
||||
Row::new()
|
||||
.push(icon::warning_icon())
|
||||
.push(Text::new(message))
|
||||
.spacing(20),
|
||||
error,
|
||||
tooltip::Position::Bottom,
|
||||
)
|
||||
.style(TooltipWarningStyle),
|
||||
))
|
||||
.padding(15)
|
||||
.center_x()
|
||||
.style(WarningStyle)
|
||||
.width(Length::Fill)
|
||||
}
|
||||
|
||||
struct WarningStyle;
|
||||
impl container::StyleSheet for WarningStyle {
|
||||
fn style(&self) -> container::Style {
|
||||
container::Style {
|
||||
border_radius: 0.0,
|
||||
text_color: iced::Color::BLACK.into(),
|
||||
background: color::WARNING.into(),
|
||||
border_color: color::WARNING,
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TooltipWarningStyle;
|
||||
impl container::StyleSheet for TooltipWarningStyle {
|
||||
fn style(&self) -> container::Style {
|
||||
container::Style {
|
||||
border_radius: 0.0,
|
||||
border_width: 1.0,
|
||||
text_color: color::WARNING.into(),
|
||||
background: color::FOREGROUND.into(),
|
||||
border_color: color::WARNING,
|
||||
}
|
||||
}
|
||||
}
|
||||
17
gui/src/ui/component/text.rs
Normal file
17
gui/src/ui/component/text.rs
Normal file
@ -0,0 +1,17 @@
|
||||
use crate::ui::font;
|
||||
|
||||
pub fn text(content: &str) -> iced::pure::widget::Text {
|
||||
iced::pure::widget::Text::new(content)
|
||||
.font(font::REGULAR)
|
||||
.size(25)
|
||||
}
|
||||
|
||||
pub trait Text {
|
||||
fn bold(self) -> Self;
|
||||
}
|
||||
|
||||
impl Text for iced::pure::widget::Text {
|
||||
fn bold(self) -> Self {
|
||||
self.font(font::BOLD)
|
||||
}
|
||||
}
|
||||
11
gui/src/ui/font.rs
Normal file
11
gui/src/ui/font.rs
Normal file
@ -0,0 +1,11 @@
|
||||
use iced::Font;
|
||||
|
||||
pub const BOLD: Font = Font::External {
|
||||
name: "Bold",
|
||||
bytes: include_bytes!("../../static/fonts/OpenSans-Bold.ttf"),
|
||||
};
|
||||
|
||||
pub const REGULAR: Font = Font::External {
|
||||
name: "Regular",
|
||||
bytes: include_bytes!("../../static/fonts/OpenSans-Regular.ttf"),
|
||||
};
|
||||
165
gui/src/ui/icon.rs
Normal file
165
gui/src/ui/icon.rs
Normal file
@ -0,0 +1,165 @@
|
||||
use iced::pure::text;
|
||||
use iced::{alignment, Font, Length, Text};
|
||||
|
||||
const ICONS: Font = Font::External {
|
||||
name: "Icons",
|
||||
bytes: include_bytes!("../../static/icons/bootstrap-icons.ttf"),
|
||||
};
|
||||
|
||||
fn icon(unicode: char) -> Text {
|
||||
text(&unicode.to_string())
|
||||
.font(ICONS)
|
||||
.width(Length::Units(20))
|
||||
.horizontal_alignment(alignment::Horizontal::Center)
|
||||
.size(20)
|
||||
}
|
||||
|
||||
pub fn vault_icon() -> Text {
|
||||
icon('\u{F65A}')
|
||||
}
|
||||
|
||||
pub fn bitcoin_icon() -> Text {
|
||||
icon('\u{F635}')
|
||||
}
|
||||
|
||||
pub fn history_icon() -> Text {
|
||||
icon('\u{F292}')
|
||||
}
|
||||
|
||||
pub fn home_icon() -> Text {
|
||||
icon('\u{F3FC}')
|
||||
}
|
||||
|
||||
pub fn unlock_icon() -> Text {
|
||||
icon('\u{F600}')
|
||||
}
|
||||
|
||||
pub fn warning_octagon_icon() -> Text {
|
||||
icon('\u{F337}')
|
||||
}
|
||||
|
||||
pub fn send_icon() -> Text {
|
||||
icon('\u{F144}')
|
||||
}
|
||||
|
||||
pub fn connect_device_icon() -> Text {
|
||||
icon('\u{F348}')
|
||||
}
|
||||
|
||||
pub fn connected_device_icon() -> Text {
|
||||
icon('\u{F350}')
|
||||
}
|
||||
|
||||
pub fn deposit_icon() -> Text {
|
||||
icon('\u{F123}')
|
||||
}
|
||||
|
||||
pub fn calendar_icon() -> Text {
|
||||
icon('\u{F1E8}')
|
||||
}
|
||||
|
||||
pub fn turnback_icon() -> Text {
|
||||
icon('\u{F131}')
|
||||
}
|
||||
|
||||
pub fn vaults_icon() -> Text {
|
||||
icon('\u{F1C7}')
|
||||
}
|
||||
|
||||
pub fn settings_icon() -> Text {
|
||||
icon('\u{F3E5}')
|
||||
}
|
||||
|
||||
pub fn block_icon() -> Text {
|
||||
icon('\u{F1C8}')
|
||||
}
|
||||
|
||||
pub fn square_icon() -> Text {
|
||||
icon('\u{F584}')
|
||||
}
|
||||
|
||||
pub fn square_check_icon() -> Text {
|
||||
icon('\u{F26D}')
|
||||
}
|
||||
|
||||
pub fn circle_check_icon() -> Text {
|
||||
icon('\u{F26B}')
|
||||
}
|
||||
|
||||
pub fn network_icon() -> Text {
|
||||
icon('\u{F40D}')
|
||||
}
|
||||
|
||||
pub fn dot_icon() -> Text {
|
||||
icon('\u{F287}')
|
||||
}
|
||||
|
||||
pub fn clipboard_icon() -> Text {
|
||||
icon('\u{F28E}')
|
||||
}
|
||||
|
||||
pub fn shield_icon() -> Text {
|
||||
icon('\u{F53F}')
|
||||
}
|
||||
|
||||
pub fn shield_notif_icon() -> Text {
|
||||
icon('\u{F530}')
|
||||
}
|
||||
|
||||
pub fn shield_check_icon() -> Text {
|
||||
icon('\u{F52F}')
|
||||
}
|
||||
|
||||
pub fn person_check_icon() -> Text {
|
||||
icon('\u{F4D6}')
|
||||
}
|
||||
|
||||
pub fn person_icon() -> Text {
|
||||
icon('\u{F4DA}')
|
||||
}
|
||||
|
||||
pub fn tooltip_icon() -> Text {
|
||||
icon('\u{F431}')
|
||||
}
|
||||
|
||||
pub fn plus_icon() -> Text {
|
||||
icon('\u{F4FE}')
|
||||
}
|
||||
|
||||
pub fn warning_icon() -> Text {
|
||||
icon('\u{F33B}')
|
||||
}
|
||||
|
||||
pub fn trash_icon() -> Text {
|
||||
icon('\u{F5DE}')
|
||||
}
|
||||
|
||||
pub fn key_icon() -> Text {
|
||||
icon('\u{F44F}')
|
||||
}
|
||||
|
||||
pub fn cross_icon() -> Text {
|
||||
icon('\u{F62A}')
|
||||
}
|
||||
|
||||
pub fn pencil_icon() -> Text {
|
||||
icon('\u{F4CB}')
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn stakeholder_icon() -> Text {
|
||||
icon('\u{F4AE}')
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn manager_icon() -> Text {
|
||||
icon('\u{F4B4}')
|
||||
}
|
||||
|
||||
pub fn done_icon() -> Text {
|
||||
icon('\u{F26B}')
|
||||
}
|
||||
|
||||
pub fn todo_icon() -> Text {
|
||||
icon('\u{F28A}')
|
||||
}
|
||||
6
gui/src/ui/mod.rs
Normal file
6
gui/src/ui/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
pub mod color;
|
||||
/// component are wrappers around iced elements;
|
||||
pub mod component;
|
||||
pub mod font;
|
||||
pub mod icon;
|
||||
pub mod util;
|
||||
28
gui/src/ui/util.rs
Normal file
28
gui/src/ui/util.rs
Normal file
@ -0,0 +1,28 @@
|
||||
/// from hecjr idea on Discord
|
||||
use iced::pure::{
|
||||
widget::{Column, Row},
|
||||
Element,
|
||||
};
|
||||
|
||||
pub trait Collection<'a, Message>: Sized {
|
||||
fn push(self, element: impl Into<Element<'a, Message>>) -> Self;
|
||||
|
||||
fn push_maybe(self, element: Option<impl Into<Element<'a, Message>>>) -> Self {
|
||||
match element {
|
||||
Some(element) => self.push(element),
|
||||
None => self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message> Collection<'a, Message> for Column<'a, Message> {
|
||||
fn push(self, element: impl Into<Element<'a, Message>>) -> Self {
|
||||
Self::push(self, element)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message> Collection<'a, Message> for Row<'a, Message> {
|
||||
fn push(self, element: impl Into<Element<'a, Message>>) -> Self {
|
||||
Self::push(self, element)
|
||||
}
|
||||
}
|
||||
202
gui/static/fonts/LICENSE.txt
Normal file
202
gui/static/fonts/LICENSE.txt
Normal file
@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
BIN
gui/static/fonts/OpenSans-Bold.ttf
Normal file
BIN
gui/static/fonts/OpenSans-Bold.ttf
Normal file
Binary file not shown.
BIN
gui/static/fonts/OpenSans-BoldItalic.ttf
Normal file
BIN
gui/static/fonts/OpenSans-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
gui/static/fonts/OpenSans-ExtraBold.ttf
Normal file
BIN
gui/static/fonts/OpenSans-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
gui/static/fonts/OpenSans-ExtraBoldItalic.ttf
Normal file
BIN
gui/static/fonts/OpenSans-ExtraBoldItalic.ttf
Normal file
Binary file not shown.
BIN
gui/static/fonts/OpenSans-Italic.ttf
Normal file
BIN
gui/static/fonts/OpenSans-Italic.ttf
Normal file
Binary file not shown.
BIN
gui/static/fonts/OpenSans-Light.ttf
Normal file
BIN
gui/static/fonts/OpenSans-Light.ttf
Normal file
Binary file not shown.
BIN
gui/static/fonts/OpenSans-LightItalic.ttf
Normal file
BIN
gui/static/fonts/OpenSans-LightItalic.ttf
Normal file
Binary file not shown.
BIN
gui/static/fonts/OpenSans-Regular.ttf
Normal file
BIN
gui/static/fonts/OpenSans-Regular.ttf
Normal file
Binary file not shown.
BIN
gui/static/fonts/OpenSans-SemiBold.ttf
Normal file
BIN
gui/static/fonts/OpenSans-SemiBold.ttf
Normal file
Binary file not shown.
BIN
gui/static/fonts/OpenSans-SemiBoldItalic.ttf
Normal file
BIN
gui/static/fonts/OpenSans-SemiBoldItalic.ttf
Normal file
Binary file not shown.
21
gui/static/icons/LICENSE.md
Normal file
21
gui/static/icons/LICENSE.md
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019-2020 The Bootstrap Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
6
gui/static/icons/README.md
Normal file
6
gui/static/icons/README.md
Normal file
@ -0,0 +1,6 @@
|
||||
# Icons
|
||||
|
||||
From the getbootstrap.com repository.
|
||||
Converted from .woff to ttf with https://raw.githubusercontent.com/hanikesn/woff2otf/master/woff2otf.py
|
||||
|
||||
Use http://mathew-kurian.github.io/CharacterMap/ to check Unicode.
|
||||
BIN
gui/static/icons/bootstrap-icons.ttf
Normal file
BIN
gui/static/icons/bootstrap-icons.ttf
Normal file
Binary file not shown.
144
gui/static/images/revault-colored-logo.svg
Normal file
144
gui/static/images/revault-colored-logo.svg
Normal file
@ -0,0 +1,144 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1326.6667 370.76001"
|
||||
height="370.76001"
|
||||
width="1326.6667"
|
||||
xml:space="preserve"
|
||||
id="svg2"
|
||||
version="1.1"><metadata
|
||||
id="metadata8"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs6"><clipPath
|
||||
id="clipPath20"
|
||||
clipPathUnits="userSpaceOnUse"><path
|
||||
id="path18"
|
||||
d="m 824.48,1471.45 c 42.012,37.8 63.149,97.51 63.149,179.56 0,81.86 -21.824,138.05 -65.301,168.61 -43.367,30.43 -119.457,45.71 -228.515,45.71 H 339.441 v -450.37 h 248.11 c 115.918,0 194.828,18.89 236.929,56.49 z m 409.06,180.64 c 0,-243.7 -96.44,-401.12 -289.306,-472.1 L 1329.38,636.121 H 911.52 L 574.434,1121.14 H 339.441 V 636.121 H 0 V 2156.89 h 576.48 c 236.454,0 405.145,-39.85 505.89,-119.71 100.75,-79.71 151.17,-208.16 151.17,-385.09 z" /></clipPath><linearGradient
|
||||
id="linearGradient28"
|
||||
spreadMethod="pad"
|
||||
gradientTransform="matrix(2790.33,2807.08,2807.08,-2790.33,-424.565,300.71)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
y2="0"
|
||||
x2="1"
|
||||
y1="0"
|
||||
x1="0"><stop
|
||||
id="stop22"
|
||||
offset="0"
|
||||
style="stop-opacity:1;stop-color:#ec2076" /><stop
|
||||
id="stop24"
|
||||
offset="0.995707"
|
||||
style="stop-opacity:1;stop-color:#f68221" /><stop
|
||||
id="stop26"
|
||||
offset="1"
|
||||
style="stop-opacity:1;stop-color:#f68221" /></linearGradient><clipPath
|
||||
id="clipPath38"
|
||||
clipPathUnits="userSpaceOnUse"><path
|
||||
id="path36"
|
||||
d="m 2678.28,2156.89 v -302.44 h -757.22 v -313.28 h 681.03 V 1251.75 H 1921.06 V 936.211 h 781.11 V 636.121 H 1581.8 V 2156.89 Z" /></clipPath><linearGradient
|
||||
id="linearGradient46"
|
||||
spreadMethod="pad"
|
||||
gradientTransform="matrix(2790.37,2807.12,2807.12,-2790.37,318.496,-437.91)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
y2="0"
|
||||
x2="1"
|
||||
y1="0"
|
||||
x1="0"><stop
|
||||
id="stop40"
|
||||
offset="0"
|
||||
style="stop-opacity:1;stop-color:#ec2076" /><stop
|
||||
id="stop42"
|
||||
offset="0.997699"
|
||||
style="stop-opacity:1;stop-color:#f68221" /><stop
|
||||
id="stop44"
|
||||
offset="1"
|
||||
style="stop-opacity:1;stop-color:#f68221" /></linearGradient><clipPath
|
||||
id="clipPath66"
|
||||
clipPathUnits="userSpaceOnUse"><path
|
||||
id="path64"
|
||||
d="m 2705.19,2482.64 c 0,-27.6 -22.61,-50.13 -50.11,-50.13 L 53.9492,2432.32 c -27.6015,0 -33.3789,15.07 -12.914,33.59 L 353.547,2747.2 c 20.461,18.4 59.723,33.48 87.332,33.48 H 2655.08 c 27.5,0 50.11,-22.54 50.11,-50.14 z" /></clipPath><linearGradient
|
||||
id="linearGradient74"
|
||||
spreadMethod="pad"
|
||||
gradientTransform="matrix(2732.06,2748.46,2748.46,-2732.06,-555.229,524.15)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
y2="0"
|
||||
x2="1"
|
||||
y1="0"
|
||||
x1="0"><stop
|
||||
id="stop68"
|
||||
offset="0"
|
||||
style="stop-opacity:1;stop-color:#ec2076" /><stop
|
||||
id="stop70"
|
||||
offset="0.995707"
|
||||
style="stop-opacity:1;stop-color:#f68221" /><stop
|
||||
id="stop72"
|
||||
offset="1"
|
||||
style="stop-opacity:1;stop-color:#f68221" /></linearGradient><clipPath
|
||||
id="clipPath84"
|
||||
clipPathUnits="userSpaceOnUse"><path
|
||||
id="path82"
|
||||
d="m 3.82031,298.109 c 0,27.539 22.52739,50.129 50.12889,50.129 L 2655.08,348.34 c 27.5,0 33.38,-15.059 12.82,-33.559 L 2355.57,33.6016 C 2335,15.1016 2295.65,0 2268.13,0 H 53.9492 C 26.3477,0 3.82031,22.6094 3.82031,50.1211 Z" /></clipPath><linearGradient
|
||||
id="linearGradient92"
|
||||
spreadMethod="pad"
|
||||
gradientTransform="matrix(2732.03,2748.43,2748.43,-2732.03,471.929,-552.45)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
y2="0"
|
||||
x2="1"
|
||||
y1="0"
|
||||
x1="0"><stop
|
||||
id="stop86"
|
||||
offset="0"
|
||||
style="stop-opacity:1;stop-color:#ec2076" /><stop
|
||||
id="stop88"
|
||||
offset="0.995707"
|
||||
style="stop-opacity:1;stop-color:#f68221" /><stop
|
||||
id="stop90"
|
||||
offset="1"
|
||||
style="stop-opacity:1;stop-color:#f68221" /></linearGradient></defs><g
|
||||
transform="matrix(1.3333333,0,0,-1.3333333,0,370.76)"
|
||||
id="g10"><g
|
||||
transform="scale(0.1)"
|
||||
id="g12"><g
|
||||
id="g14"><g
|
||||
clip-path="url(#clipPath20)"
|
||||
id="g16"><path
|
||||
id="path30"
|
||||
style="fill:url(#linearGradient28);fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 824.48,1471.45 c 42.012,37.8 63.149,97.51 63.149,179.56 0,81.86 -21.824,138.05 -65.301,168.61 -43.367,30.43 -119.457,45.71 -228.515,45.71 H 339.441 v -450.37 h 248.11 c 115.918,0 194.828,18.89 236.929,56.49 z m 409.06,180.64 c 0,-243.7 -96.44,-401.12 -289.306,-472.1 L 1329.38,636.121 H 911.52 L 574.434,1121.14 H 339.441 V 636.121 H 0 V 2156.89 h 576.48 c 236.454,0 405.145,-39.85 505.89,-119.71 100.75,-79.71 151.17,-208.16 151.17,-385.09" /></g></g><g
|
||||
id="g32"><g
|
||||
clip-path="url(#clipPath38)"
|
||||
id="g34"><path
|
||||
id="path48"
|
||||
style="fill:url(#linearGradient46);fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 2678.28,2156.89 v -302.44 h -757.22 v -313.28 h 681.03 V 1251.75 H 1921.06 V 936.211 h 781.11 V 636.121 H 1581.8 V 2156.89 h 1096.48" /></g></g><path
|
||||
id="path50"
|
||||
style="fill:#181818;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="M 3849.36,636.121 H 3562.19 L 2950.76,2156.89 h 287.26 l 467.79,-1135.71 467.81,1135.71 h 287.17 L 3849.36,636.121" /><path
|
||||
id="path52"
|
||||
style="fill:#181818;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 5463.67,1219.07 -276.21,626.57 -276.4,-626.57 z M 4806.61,982.02 4654.35,636.121 h -274.14 l 670.16,1520.769 h 274.16 L 5994.62,636.121 H 5720.46 L 5568.23,982.02 h -761.62" /><path
|
||||
id="path54"
|
||||
style="fill:#181818;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 6510.1,984.07 c 69.62,-81.269 163.97,-121.82 282.96,-121.82 118.77,0 213.24,40.551 282.66,121.82 69.72,81.34 104.56,191.5 104.56,330.84 v 841.98 h 256.81 v -852.86 c 0,-219.14 -60.3,-387.71 -180.65,-505.889 -120.4,-118.379 -274.92,-177.321 -463.38,-177.321 -188.68,0 -343.27,58.942 -463.48,177.321 -120.45,118.179 -180.76,286.749 -180.76,505.889 v 852.86 h 256.81 v -841.98 c 0,-139.34 34.85,-249.5 104.47,-330.84" /><path
|
||||
id="path56"
|
||||
style="fill:#181818;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="M 7839.38,636.121 V 2156.89 h 256.84 V 879.699 h 694.04 V 636.121 h -950.88" /><path
|
||||
id="path58"
|
||||
style="fill:#181818;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="M 9488.74,1921.84 V 636.121 H 9232.15 V 1921.84 h -461.38 v 235.05 H 9950 v -235.05 h -461.26" /><g
|
||||
id="g60"><g
|
||||
clip-path="url(#clipPath66)"
|
||||
id="g62"><path
|
||||
id="path76"
|
||||
style="fill:url(#linearGradient74);fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 2705.19,2482.64 c 0,-27.6 -22.61,-50.13 -50.11,-50.13 L 53.9492,2432.32 c -27.6015,0 -33.3789,15.07 -12.914,33.59 L 353.547,2747.2 c 20.461,18.4 59.723,33.48 87.332,33.48 H 2655.08 c 27.5,0 50.11,-22.54 50.11,-50.14 v -247.9" /></g></g><g
|
||||
id="g78"><g
|
||||
clip-path="url(#clipPath84)"
|
||||
id="g80"><path
|
||||
id="path94"
|
||||
style="fill:url(#linearGradient92);fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 3.82031,298.109 c 0,27.539 22.52739,50.129 50.12889,50.129 L 2655.08,348.34 c 27.5,0 33.38,-15.059 12.82,-33.559 L 2355.57,33.6016 C 2335,15.1016 2295.65,0 2268.13,0 H 53.9492 C 26.3477,0 3.82031,22.6094 3.82031,50.1211 V 298.109" /></g></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 7.9 KiB |
Loading…
x
Reference in New Issue
Block a user