├── hwi ├── src │ ├── app │ │ ├── mod.rs │ │ └── revault.rs │ ├── lib.rs │ └── specter.rs └── Cargo.toml ├── .gitignore ├── ui ├── static │ ├── fonts │ │ ├── OpenSans-Bold.ttf │ │ ├── OpenSans-Light.ttf │ │ ├── OpenSans-Italic.ttf │ │ ├── OpenSans-Regular.ttf │ │ ├── OpenSans-BoldItalic.ttf │ │ ├── OpenSans-ExtraBold.ttf │ │ ├── OpenSans-SemiBold.ttf │ │ ├── OpenSans-LightItalic.ttf │ │ ├── OpenSans-ExtraBoldItalic.ttf │ │ └── OpenSans-SemiBoldItalic.ttf │ ├── icons │ │ ├── bootstrap-icons.ttf │ │ ├── README.md │ │ └── LICENSE.md │ └── images │ │ └── revault-colored-logo.svg ├── src │ ├── lib.rs │ ├── component │ │ ├── image.rs │ │ ├── text.rs │ │ ├── notification.rs │ │ ├── form.rs │ │ └── button.rs │ ├── font.rs │ ├── util.rs │ ├── color.rs │ └── icon.rs └── Cargo.toml ├── screenshots ├── revault-gui-manager.png └── revault-gui-stakeholder.png ├── src ├── lib.rs ├── app │ ├── menu.rs │ ├── state │ │ ├── mod.rs │ │ ├── cmd.rs │ │ ├── vault.rs │ │ ├── deposit.rs │ │ ├── emergency.rs │ │ ├── revault.rs │ │ ├── vaults.rs │ │ └── sign.rs │ ├── view │ │ ├── mod.rs │ │ ├── deposit.rs │ │ ├── sign.rs │ │ ├── warning.rs │ │ ├── vaults.rs │ │ ├── layout.rs │ │ ├── emergency.rs │ │ └── revault.rs │ ├── error.rs │ ├── config.rs │ ├── message.rs │ ├── context.rs │ └── mod.rs ├── revault.rs ├── conversion.rs ├── installer │ ├── message.rs │ ├── config.rs │ └── step │ │ └── common.rs └── daemon │ ├── model.rs │ ├── client │ ├── error.rs │ └── jsonrpc.rs │ └── mod.rs ├── tests ├── utils │ ├── mod.rs │ ├── sandbox.rs │ ├── fixtures.rs │ └── mock.rs └── app_revault.rs ├── contrib ├── revault_gui.toml ├── coverage.sh └── tools │ └── dummysigner │ ├── Cargo.toml │ ├── src │ ├── main.rs │ ├── config.rs │ ├── server.rs │ └── api.rs │ ├── examples │ ├── examples_cfg.toml │ ├── manager_client.rs │ ├── stakeholder_batch.rs │ └── stakeholder_client.rs │ └── README.md ├── Cargo.toml ├── .github └── workflows │ └── main.yml ├── LICENSE └── README.md /hwi/src/app/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "revault")] 2 | pub mod revault; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | contrib/tools/dummysigner/target 3 | conf*.toml 4 | -------------------------------------------------------------------------------- /ui/static/fonts/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revault/revault-gui/HEAD/ui/static/fonts/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /ui/static/fonts/OpenSans-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revault/revault-gui/HEAD/ui/static/fonts/OpenSans-Light.ttf -------------------------------------------------------------------------------- /screenshots/revault-gui-manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revault/revault-gui/HEAD/screenshots/revault-gui-manager.png -------------------------------------------------------------------------------- /ui/static/fonts/OpenSans-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revault/revault-gui/HEAD/ui/static/fonts/OpenSans-Italic.ttf -------------------------------------------------------------------------------- /ui/static/fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revault/revault-gui/HEAD/ui/static/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /ui/static/icons/bootstrap-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revault/revault-gui/HEAD/ui/static/icons/bootstrap-icons.ttf -------------------------------------------------------------------------------- /screenshots/revault-gui-stakeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revault/revault-gui/HEAD/screenshots/revault-gui-stakeholder.png -------------------------------------------------------------------------------- /ui/static/fonts/OpenSans-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revault/revault-gui/HEAD/ui/static/fonts/OpenSans-BoldItalic.ttf -------------------------------------------------------------------------------- /ui/static/fonts/OpenSans-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revault/revault-gui/HEAD/ui/static/fonts/OpenSans-ExtraBold.ttf -------------------------------------------------------------------------------- /ui/static/fonts/OpenSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revault/revault-gui/HEAD/ui/static/fonts/OpenSans-SemiBold.ttf -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod app; 2 | pub mod conversion; 3 | pub mod daemon; 4 | pub mod installer; 5 | pub mod loader; 6 | pub mod revault; 7 | -------------------------------------------------------------------------------- /ui/static/fonts/OpenSans-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revault/revault-gui/HEAD/ui/static/fonts/OpenSans-LightItalic.ttf -------------------------------------------------------------------------------- /ui/static/fonts/OpenSans-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revault/revault-gui/HEAD/ui/static/fonts/OpenSans-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /ui/static/fonts/OpenSans-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revault/revault-gui/HEAD/ui/static/fonts/OpenSans-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /ui/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod color; 2 | /// component are wrappers around iced elements; 3 | pub mod component; 4 | pub mod font; 5 | pub mod icon; 6 | pub mod util; 7 | -------------------------------------------------------------------------------- /ui/static/icons/README.md: -------------------------------------------------------------------------------- 1 | # Icons 2 | 3 | From the getbootstrap.com repository. 4 | Converted from .woff to ttf with https://raw.githubusercontent.com/hanikesn/woff2otf/master/woff2otf.py 5 | 6 | Use http://mathew-kurian.github.io/CharacterMap/ to check Unicode. 7 | -------------------------------------------------------------------------------- /ui/src/component/image.rs: -------------------------------------------------------------------------------- 1 | use iced::widget::svg::{Handle, Svg}; 2 | 3 | const LOGO: &[u8] = include_bytes!("../../static/images/revault-colored-logo.svg"); 4 | 5 | pub fn revault_colored_logo() -> Svg { 6 | let h = Handle::from_memory(LOGO.to_vec()); 7 | Svg::new(h) 8 | } 9 | -------------------------------------------------------------------------------- /tests/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod fixtures; 2 | pub mod mock; 3 | pub mod sandbox; 4 | 5 | use revault_hwi::{app::revault::RevaultHWI, HWIError}; 6 | 7 | pub async fn no_hardware_wallet() -> Result, HWIError> { 8 | Err(HWIError::DeviceNotFound) 9 | } 10 | -------------------------------------------------------------------------------- /ui/src/font.rs: -------------------------------------------------------------------------------- 1 | use iced::Font; 2 | 3 | pub const BOLD: Font = Font::External { 4 | name: "Bold", 5 | bytes: include_bytes!("../static/fonts/OpenSans-Bold.ttf"), 6 | }; 7 | 8 | pub const REGULAR: Font = Font::External { 9 | name: "Regular", 10 | bytes: include_bytes!("../static/fonts/OpenSans-Regular.ttf"), 11 | }; 12 | -------------------------------------------------------------------------------- /contrib/revault_gui.toml: -------------------------------------------------------------------------------- 1 | # Revault GUI configuration file example. 2 | 3 | # Path to revaultd configuration file (required). 4 | revaultd_config_path = "path/to/revault.toml" 5 | # Path to revaultd binary (optional). 6 | revaultd_path = "path/to/revaultd/binary" 7 | # log level, can be "info", "debug", "trace" (optional). 8 | log_level = "trace" 9 | # Use iced debug feature if true (optional). 10 | debug = true 11 | -------------------------------------------------------------------------------- /src/app/menu.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, PartialEq, Eq)] 2 | pub enum Menu { 3 | History, 4 | Deposit, 5 | Emergency, 6 | Home, 7 | Send, 8 | CreateSpend, 9 | ImportSpend, 10 | CreateVaults, 11 | RevaultVaults, 12 | DelegateFunds, 13 | Settings, 14 | Vaults(VaultsMenu), 15 | } 16 | 17 | #[derive(Debug, Clone, PartialEq, Eq)] 18 | pub enum VaultsMenu { 19 | Current, 20 | Moving, 21 | Moved, 22 | } 23 | -------------------------------------------------------------------------------- /ui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "revault_ui" 3 | version = "0.1.0" 4 | description = "Iced components for Revault GUI" 5 | repository = "https://github.com/revault/revault-gui" 6 | license = "BSD-3-Clause" 7 | authors = ["Edouard Paris ", "Daniela Brozzoni "] 8 | edition = "2018" 9 | resolver = "2" 10 | 11 | [dependencies] 12 | iced = { version = "0.4", default-features= false, features = ["wgpu", "svg"] } 13 | -------------------------------------------------------------------------------- /src/revault.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 2 | pub enum Role { 3 | Manager, 4 | Stakeholder, 5 | } 6 | 7 | impl std::fmt::Display for Role { 8 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 9 | write!( 10 | f, 11 | "{}", 12 | match self { 13 | Role::Manager => "Manager", 14 | Role::Stakeholder => "Stakeholder", 15 | } 16 | ) 17 | } 18 | } 19 | 20 | impl Role { 21 | pub const ALL: [Role; 2] = [Role::Manager, Role::Stakeholder]; 22 | pub const MANAGER_ONLY: [Role; 1] = [Role::Manager]; 23 | pub const STAKEHOLDER_ONLY: [Role; 1] = [Role::Stakeholder]; 24 | pub const STAKEHOLDER_AND_MANAGER: [Role; 2] = [Role::Stakeholder, Role::Manager]; 25 | } 26 | -------------------------------------------------------------------------------- /contrib/coverage.sh: -------------------------------------------------------------------------------- 1 | set -ex 2 | 3 | rustup component add llvm-tools-preview 4 | if ! command -v grcov &>/dev/null; then 5 | cargo +nightly install grcov 6 | fi 7 | 8 | cargo clean 9 | 10 | rm -f revault_gui_coverage_*.profraw 11 | LLVM_PROFILE_FILE="revault_gui_coverage_%m.profraw" RUSTFLAGS="-Zinstrument-coverage" RUSTDOCFLAGS="$RUSTFLAGS -Z unstable-options --persist-doctests target/debug/doctestbins" cargo +nightly build --all-features 12 | LLVM_PROFILE_FILE="revault_gui_coverage_%m.profraw" RUSTFLAGS="-Zinstrument-coverage" RUSTDOCFLAGS="$RUSTFLAGS -Z unstable-options --persist-doctests target/debug/doctestbins" cargo +nightly test --all-features 13 | 14 | grcov . --binary-path ./target/debug/ -t html --branch --ignore-not-existing --llvm -o ./target/grcov/ 15 | firefox target/grcov/index.html 16 | 17 | set +ex 18 | -------------------------------------------------------------------------------- /ui/src/util.rs: -------------------------------------------------------------------------------- 1 | /// from hecjr idea on Discord 2 | use iced::{Column, Element, Row}; 3 | 4 | pub trait Collection<'a, Message>: Sized { 5 | fn push(self, element: impl Into>) -> Self; 6 | 7 | fn push_maybe(self, element: Option>>) -> Self { 8 | match element { 9 | Some(element) => self.push(element), 10 | None => self, 11 | } 12 | } 13 | } 14 | 15 | impl<'a, Message> Collection<'a, Message> for Column<'a, Message> { 16 | fn push(self, element: impl Into>) -> Self { 17 | Self::push(self, element) 18 | } 19 | } 20 | 21 | impl<'a, Message> Collection<'a, Message> for Row<'a, Message> { 22 | fn push(self, element: impl Into>) -> Self { 23 | Self::push(self, element) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /contrib/tools/dummysigner/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dummysigner" 3 | version = "0.1.0" 4 | edition = "2018" 5 | resolver = "2" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | # add empty workspace to keep dummysigner out of the revault-gui crates 10 | [workspace] 11 | 12 | [dependencies] 13 | revault_tx = { version = "0.5.0", features = ["use-serde"] } 14 | 15 | base64 = "0.13.0" 16 | iced = {version = "0.4", default-features = false, features = ["wgpu", "tokio"]} 17 | iced_futures = "0.4" 18 | iced_native = "0.5" 19 | serde = { version = "1.0", features = ["derive"] } 20 | serde_json = "1" 21 | tokio = {version = "1.9.0", features = ["net", "io-util"]} 22 | tokio-util = { version = "0.6", features = ["codec"] } 23 | tokio-serde = {version = "0.8", features = ["json"]} 24 | toml = "0.5" 25 | 26 | [dev-dependencies] 27 | futures = "0.3" 28 | tokio = {version = "1.9.0", features = ["macros", "net", "io-util"]} 29 | tokio-util = { version = "0.6", features = ["codec"] } 30 | tokio-serde = {version = "0.8", features = ["json"]} 31 | serde_json = "1" 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/utils/sandbox.rs: -------------------------------------------------------------------------------- 1 | use iced_native::command::Action; 2 | use revault_gui::app::{context::Context, message::Message, state::State}; 3 | 4 | pub struct Sandbox { 5 | state: S, 6 | } 7 | 8 | impl Sandbox { 9 | pub fn new(state: S) -> Self { 10 | return Self { state }; 11 | } 12 | 13 | pub fn state(&self) -> &S { 14 | &self.state 15 | } 16 | 17 | pub async fn update(mut self, ctx: &Context, message: Message) -> Self { 18 | let cmd = self.state.update(ctx, message); 19 | for action in cmd.actions() { 20 | if let Action::Future(f) = action { 21 | let msg = f.await; 22 | let _cmd = self.state.update(ctx, msg); 23 | } 24 | } 25 | 26 | self 27 | } 28 | 29 | pub async fn load(mut self, ctx: &Context) -> Self { 30 | let cmd = self.state.load(ctx); 31 | for action in cmd.actions() { 32 | if let Action::Future(f) = action { 33 | let msg = f.await; 34 | self = self.update(ctx, msg).await; 35 | } 36 | } 37 | 38 | self 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ui/static/icons/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-2020 The Bootstrap Authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "revault-gui" 3 | version = "0.4.0" 4 | readme = "README.md" 5 | description = "Revault GUI" 6 | repository = "https://github.com/revault/revault-gui" 7 | license = "BSD-3-Clause" 8 | authors = ["Edouard Paris ", "Daniela Brozzoni "] 9 | edition = "2018" 10 | resolver = "2" 11 | 12 | [workspace] 13 | members = [ 14 | "ui", 15 | "hwi" 16 | ] 17 | 18 | [[bin]] 19 | name = "revault-gui" 20 | path = "src/main.rs" 21 | 22 | [dependencies] 23 | bitcoin = { version = "0.27", features = ["base64", "use-serde"] } 24 | revaultd = { version = "0.4.0", default-features = false} 25 | backtrace = "0.3" 26 | 27 | iced = { version = "0.4", default-features= false, features = ["tokio", "wgpu", "svg", "qr_code"] } 28 | iced_native = "0.5" 29 | revault_ui = { path = "./ui" } 30 | revault_hwi = { path = "./hwi" } 31 | 32 | tokio = {version = "1.9.0", features = ["signal"]} 33 | serde = { version = "1.0", features = ["derive"] } 34 | serde_json = "1.0" 35 | 36 | # Logging stuff 37 | log = "0.4" 38 | fern = "0.6" 39 | 40 | dirs = "3.0.1" 41 | toml = "0.5" 42 | 43 | chrono = "0.4" 44 | 45 | [target.'cfg(windows)'.dependencies] 46 | uds_windows = "0.1.5" 47 | 48 | [dev-dependencies] 49 | tokio = {version = "1.9.0", features = ["rt", "macros"]} 50 | -------------------------------------------------------------------------------- /hwi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "revault_hwi" 3 | version = "0.1.0" 4 | description = "Hardware wallet interface for Revault GUI" 5 | repository = "https://github.com/revault/revault-gui" 6 | license = "BSD-3-Clause" 7 | authors = ["Edouard Paris ", "Daniela Brozzoni "] 8 | edition = "2018" 9 | 10 | [features] 11 | default = ["revault", "dummysigner", "specter"] 12 | revault = [] 13 | dummysigner = ["log", "tokio", "tokio-util", "tokio-serde", "serde", "serde_json"] 14 | specter = ["tokio", "tokio-serial", "serialport"] 15 | 16 | [dependencies] 17 | async-trait = "0.1.52" 18 | futures = "0.3" 19 | bitcoin = { version = "0.27", features = ["base64", "use-serde"] } 20 | 21 | 22 | # dummysigner 23 | # specter 24 | tokio = { version = "1.9.0", features = ["net", "io-util"], optional = true } 25 | 26 | # dummysigner 27 | log = { version = "0.4", optional = true } 28 | tokio-util = { version = "0.6", features = ["codec"], optional = true } 29 | tokio-serde = {version = "0.8", features = ["json"], optional = true } 30 | serde = { version = "1.0", features = ["derive"], optional = true } 31 | serde_json = { version ="1.0", optional = true } 32 | 33 | # specter 34 | tokio-serial = { version = "5.4.1", optional = true } 35 | serialport = { version = "4", optional = true } 36 | 37 | -------------------------------------------------------------------------------- /src/app/state/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cmd; 2 | mod deposit; 3 | mod emergency; 4 | pub mod history; 5 | pub mod manager; 6 | mod revault; 7 | mod settings; 8 | mod sign; 9 | mod spend_transaction; 10 | pub mod stakeholder; 11 | mod vault; 12 | mod vaults; 13 | 14 | use iced::{Command, Element, Subscription}; 15 | 16 | pub use deposit::DepositState; 17 | pub use emergency::EmergencyState; 18 | pub use history::HistoryState; 19 | pub use manager::{ 20 | ManagerCreateSendTransactionState, ManagerHomeState, ManagerImportSendTransactionState, 21 | ManagerSendState, 22 | }; 23 | pub use revault::RevaultVaultsState; 24 | pub use settings::SettingsState; 25 | pub use spend_transaction::{SpendTransactionListItem, SpendTransactionState}; 26 | pub use stakeholder::{ 27 | StakeholderCreateVaultsState, StakeholderDelegateVaultsState, StakeholderHomeState, 28 | }; 29 | pub use vaults::VaultsState; 30 | 31 | use super::{context::Context, message::Message}; 32 | 33 | pub trait State { 34 | fn view(&mut self, ctx: &Context) -> Element; 35 | fn update(&mut self, ctx: &Context, message: Message) -> Command; 36 | fn subscription(&self) -> Subscription { 37 | Subscription::none() 38 | } 39 | fn load(&self, _ctx: &Context) -> Command { 40 | Command::none() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/conversion.rs: -------------------------------------------------------------------------------- 1 | use bitcoin::Network; 2 | 3 | /// Converter purpose is to give a Conversion from a given amount in satoshis according to its 4 | /// parameters. 5 | pub struct Converter { 6 | pub unit: Unit, 7 | } 8 | 9 | impl Converter { 10 | pub fn new(bitcoin_network: Network) -> Self { 11 | let unit = match bitcoin_network { 12 | Network::Testnet => Unit::TestnetBitcoin, 13 | Network::Bitcoin => Unit::Bitcoin, 14 | Network::Regtest => Unit::RegtestBitcoin, 15 | Network::Signet => Unit::SignetBitcoin, 16 | }; 17 | Self { unit } 18 | } 19 | 20 | /// converts amount in satoshis to BTC float. 21 | pub fn converts(&self, amount: bitcoin::Amount) -> String { 22 | format!("{:.8}", amount.as_btc()) 23 | } 24 | } 25 | 26 | /// Unit is the bitcoin ticker according to the network used. 27 | pub enum Unit { 28 | TestnetBitcoin, 29 | RegtestBitcoin, 30 | SignetBitcoin, 31 | Bitcoin, 32 | } 33 | 34 | impl std::fmt::Display for Unit { 35 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 36 | match self { 37 | Self::TestnetBitcoin => write!(f, "tBTC"), 38 | Self::RegtestBitcoin => write!(f, "rBTC"), 39 | Self::SignetBitcoin => write!(f, "sBTC"), 40 | Self::Bitcoin => write!(f, "BTC"), 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /hwi/src/lib.rs: -------------------------------------------------------------------------------- 1 | use bitcoin::util::psbt::PartiallySignedTransaction as Psbt; 2 | 3 | pub mod app; 4 | 5 | #[cfg(feature = "dummysigner")] 6 | pub mod dummysigner; 7 | 8 | #[cfg(feature = "specter")] 9 | pub mod specter; 10 | 11 | use async_trait::async_trait; 12 | use std::fmt::Debug; 13 | 14 | #[derive(Debug, Clone)] 15 | pub enum HWIError { 16 | UnimplementedMethod, 17 | DeviceDisconnected, 18 | DeviceNotFound, 19 | DeviceDidNotSign, 20 | Device(String), 21 | } 22 | 23 | impl std::fmt::Display for HWIError { 24 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 25 | match self { 26 | HWIError::UnimplementedMethod => write!(f, "unimplemented method"), 27 | HWIError::DeviceDisconnected => write!(f, "device disconnected"), 28 | HWIError::DeviceNotFound => write!(f, "device not found"), 29 | HWIError::DeviceDidNotSign => write!(f, "device did not sign"), 30 | HWIError::Device(e) => write!(f, "{}", e), 31 | } 32 | } 33 | } 34 | 35 | /// HWI is the common Hardware Wallet Interface. 36 | #[async_trait] 37 | pub trait HWI: Debug { 38 | /// Check that the device is connected but not necessarily available. 39 | async fn is_connected(&mut self) -> Result<(), HWIError>; 40 | /// Sign a partially signed bitcoin transaction (PSBT). 41 | async fn sign_tx(&mut self, tx: &Psbt) -> Result; 42 | } 43 | -------------------------------------------------------------------------------- /ui/src/component/text.rs: -------------------------------------------------------------------------------- 1 | use super::{color, font}; 2 | use iced::{alignment, Color, Element, Length}; 3 | 4 | pub struct Text(iced::Text); 5 | 6 | impl Text { 7 | pub fn new(content: &str) -> Self { 8 | Self(iced::Text::new(content).font(font::REGULAR).size(25)) 9 | } 10 | 11 | pub fn bold(mut self) -> Self { 12 | self.0 = self.0.font(font::BOLD); 13 | self 14 | } 15 | 16 | pub fn small(mut self) -> Self { 17 | self.0 = self.0.size(20); 18 | self 19 | } 20 | 21 | pub fn size(mut self, i: u16) -> Self { 22 | self.0 = self.0.size(i); 23 | self 24 | } 25 | 26 | pub fn success(mut self) -> Self { 27 | self.0 = self.0.color(color::SUCCESS); 28 | self 29 | } 30 | pub fn horizontal_alignment(mut self, alignment: alignment::Horizontal) -> Self { 31 | self.0 = self.0.horizontal_alignment(alignment); 32 | self 33 | } 34 | pub fn width(mut self, width: Length) -> Self { 35 | self.0 = self.0.width(width); 36 | self 37 | } 38 | 39 | pub fn color>(mut self, color: C) -> Self { 40 | self.0 = self.0.color(color); 41 | self 42 | } 43 | } 44 | 45 | impl<'a, Message> From for Element<'a, Message> { 46 | fn from(text: Text) -> Element<'a, Message> { 47 | text.0.into() 48 | } 49 | } 50 | 51 | impl From for Text { 52 | fn from(text: iced::Text) -> Text { 53 | Text(text) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /contrib/tools/dummysigner/src/main.rs: -------------------------------------------------------------------------------- 1 | mod api; 2 | mod app; 3 | mod config; 4 | mod server; 5 | mod sign; 6 | mod view; 7 | 8 | use std::env; 9 | use std::path::PathBuf; 10 | use std::process; 11 | use std::str::FromStr; 12 | 13 | use revault_tx::miniscript::descriptor::DescriptorSecretKey; 14 | 15 | fn main() { 16 | let args: Vec = env::args().collect(); 17 | if args.len() < 2 { 18 | eprintln!( 19 | "Usage:\n{} ...\n{} --conf ", 20 | args[0], args[0] 21 | ); 22 | process::exit(1); 23 | } 24 | 25 | let cfg = if args[1] == "--conf" || args[1] == "-c" { 26 | let path = &args[2]; 27 | match config::Config::from_file(&PathBuf::from(path)) { 28 | Ok(cfg) => cfg, 29 | Err(e) => { 30 | eprintln!("{}", e); 31 | process::exit(1); 32 | } 33 | } 34 | } else { 35 | let mut keys = Vec::new(); 36 | for arg in &args[1..] { 37 | let key = match DescriptorSecretKey::from_str(arg) { 38 | Ok(DescriptorSecretKey::XPrv(xpriv)) => xpriv.xkey, 39 | _ => { 40 | eprintln!("{} is not a xpriv", arg); 41 | process::exit(1); 42 | } 43 | }; 44 | keys.push(key); 45 | } 46 | config::Config::new(keys) 47 | }; 48 | 49 | if let Err(e) = app::run(cfg) { 50 | println!("{}", e); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ui/src/component/notification.rs: -------------------------------------------------------------------------------- 1 | use crate::{color, icon}; 2 | use iced::{container, tooltip, Container, Length, Row, Text, Tooltip}; 3 | 4 | pub fn warning<'a, T: 'a>(message: &str, error: &str) -> Container<'a, T> { 5 | Container::new(Container::new( 6 | Tooltip::new( 7 | Row::new() 8 | .push(icon::warning_icon()) 9 | .push(Text::new(message)) 10 | .spacing(20), 11 | error, 12 | tooltip::Position::Bottom, 13 | ) 14 | .style(TooltipWarningStyle), 15 | )) 16 | .padding(15) 17 | .center_x() 18 | .style(WarningStyle) 19 | .width(Length::Fill) 20 | } 21 | 22 | struct WarningStyle; 23 | impl container::StyleSheet for WarningStyle { 24 | fn style(&self) -> container::Style { 25 | container::Style { 26 | border_radius: 0.0, 27 | text_color: iced::Color::BLACK.into(), 28 | background: color::WARNING.into(), 29 | border_color: color::WARNING, 30 | ..container::Style::default() 31 | } 32 | } 33 | } 34 | 35 | struct TooltipWarningStyle; 36 | impl container::StyleSheet for TooltipWarningStyle { 37 | fn style(&self) -> container::Style { 38 | container::Style { 39 | border_radius: 0.0, 40 | border_width: 1.0, 41 | text_color: color::WARNING.into(), 42 | background: color::FOREGROUND.into(), 43 | border_color: color::WARNING, 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | # We don't have a MSRV (yet?) 10 | toolchain: 11 | - stable 12 | - beta 13 | - nightly 14 | os: 15 | - ubuntu-latest 16 | - macos-latest 17 | - windows-latest 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - name: Install needed libraries 21 | if: matrix.os == 'ubuntu-latest' 22 | run: sudo apt-get update & sudo apt-get install --allow-downgrades libudev1=245.4-4ubuntu3 libudev-dev=245.4-4ubuntu3 pkg-config libxkbcommon-dev libvulkan-dev 23 | - name: Checkout source code 24 | uses: actions/checkout@v2 25 | - name: Install Rust ${{ matrix.toolchain }} toolchain 26 | uses: actions-rs/toolchain@v1 27 | with: 28 | toolchain: ${{ matrix.toolchain }} 29 | override: true 30 | profile: minimal 31 | - name: Build on Rust ${{ matrix.toolchain }} 32 | run: cargo build --release --verbose --color always 33 | - name: Test on Rust ${{ matrix.toolchain }} 34 | run: cargo test --release --verbose --color always 35 | 36 | rustfmt_check: 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v1 40 | - uses: actions-rs/toolchain@v1 41 | with: 42 | toolchain: stable 43 | components: rustfmt 44 | override: true 45 | - run: cargo fmt -- --check 46 | -------------------------------------------------------------------------------- /contrib/tools/dummysigner/examples/examples_cfg.toml: -------------------------------------------------------------------------------- 1 | # optional if you are a manager 2 | emergency_address = "bcrt1qewc2348370pgw8kjz8gy09z8xyh0d9fxde6nzamd3txc9gkmjqmq8m4cdq" 3 | 4 | # your extended private keys 5 | [[keys]] 6 | name = "stk1" 7 | xpriv = "xprv9zFeRZgUZaUZBEUq1vPFLpUavHPK5YZ6N2qeqCYe7GLxGVY9SRHuN5Uwd5YN56tMUKe2qPhmvP8fC1GBEAFRAwbJQi86swWvvGM5tXBpJt6" 8 | 9 | [[keys]] 10 | name = "stk2" 11 | xpriv = "xprvA27zVGM23jVKNzHhtfX3rd244KovFwAadjBz19Ke396eFBUWsVQYLfk7FAK6dENumsrtd8mJCSFxnm9BkyaWXuBVSd5tZ2c9r5tjPNkz7A9" 12 | 13 | [[keys]] 14 | name = "man1" 15 | xpriv = "xprv9yZtssy7SLcqLCQetFHe35ENDKT58vh87MNFYdNkMyphUAcfCXdxLzgG4enc7ZT8NXjBtivtLrtpjZAJzyiTEAKM6NKUeFerP97DZdctJPr" 16 | 17 | 18 | [descriptors] 19 | cpfp_descriptor = "wsh(thresh(1,pk(xpub6Doj75MBvKp7bgHxF1KeDGxm36rd4wonZWv8sfzTeNoNVX2QZaQdrEcs7NDXvs4Cbsy9TPMx5VDcMK6JjSKepBbYDPiJ9bLBR4bqfdHmxZx/*)))#r9m50cqk" 20 | deposit_descriptor = "wsh(multi(2,xpub6DEzq5DNPx2rPiZJ7wvFhxRKUKDoV1GwjFmFdaxFfbsw9HsHyxc9usoRUMxqJaMrwoXh4apahsGEnjAS4cVCBDgqsx5Groww22AdHbgxVDg/*,xpub6F7Ltmsut73cbUNAzh44DkxncMeQfPtRzx7aoXjFbUdd7yofR2intU4b6QcsXot1jgmVjHB3iMybCLhtqvhAx3L4VPbGUz5fwuyNeTkypUP/*))#asfyu8z9" 21 | unvault_descriptor = "wsh(andor(thresh(1,pk(xpub6CZFHPW1GiB8YgV7zGpeQDB6mMHZYPQyUaHrM1nMvKMgLxwok4xCtnzjuxQ3p1LHJUkz5i1Y7bRy5fmGrdg8UBVb39XdXNtWWd2wTsNd7T9/*)),older(8),thresh(2,pkh(xpub6DEzq5DNPx2rPiZJ7wvFhxRKUKDoV1GwjFmFdaxFfbsw9HsHyxc9usoRUMxqJaMrwoXh4apahsGEnjAS4cVCBDgqsx5Groww22AdHbgxVDg/*),a:pkh(xpub6F7Ltmsut73cbUNAzh44DkxncMeQfPtRzx7aoXjFbUdd7yofR2intU4b6QcsXot1jgmVjHB3iMybCLhtqvhAx3L4VPbGUz5fwuyNeTkypUP/*))))#7kgsjsga" 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Revault 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /src/app/view/mod.rs: -------------------------------------------------------------------------------- 1 | mod deposit; 2 | mod emergency; 3 | mod history; 4 | mod home; 5 | mod layout; 6 | pub mod manager; 7 | mod revault; 8 | pub mod settings; 9 | mod sidebar; 10 | pub mod sign; 11 | pub mod spend_transaction; 12 | pub mod stakeholder; 13 | pub mod vault; 14 | mod vaults; 15 | mod warning; 16 | 17 | pub use deposit::DepositView; 18 | pub use emergency::{EmergencyTriggeredView, EmergencyView}; 19 | pub use history::{HistoryEventListItemView, HistoryEventView, HistoryView}; 20 | pub use home::{ManagerHomeView, StakeholderHomeView}; 21 | pub use revault::{RevaultSelectVaultsView, RevaultSuccessView, RevaultVaultListItemView}; 22 | pub use settings::SettingsView; 23 | pub use spend_transaction::{SpendTransactionListItemView, SpendTransactionView}; 24 | pub use stakeholder::{ 25 | StakeholderCreateVaultsView, StakeholderDelegateVaultsView, 26 | StakeholderSelecteVaultsToDelegateView, 27 | }; 28 | pub use vault::VaultView; 29 | pub use vaults::VaultsView; 30 | 31 | use iced::{Column, Element}; 32 | 33 | use crate::app::{context::Context, error::Error, message::Message}; 34 | 35 | #[derive(Debug, Default)] 36 | pub struct LoadingDashboard { 37 | dashboard: layout::Dashboard, 38 | } 39 | 40 | impl LoadingDashboard { 41 | pub fn view<'a>(&'a mut self, ctx: &Context, warning: Option<&Error>) -> Element<'a, Message> { 42 | self.dashboard.view(ctx, warning, Column::new()) 43 | } 44 | } 45 | 46 | #[derive(Debug, Default)] 47 | pub struct LoadingModal { 48 | modal: layout::Modal, 49 | } 50 | 51 | impl LoadingModal { 52 | pub fn view<'a, T: Into>( 53 | &'a mut self, 54 | ctx: &Context, 55 | warning: Option<&Error>, 56 | close_redirect: T, 57 | ) -> Element<'a, Message> { 58 | self.modal 59 | .view(ctx, warning, Column::new(), None, close_redirect.into()) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/installer/message.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use super::Error; 4 | use crate::revault::Role; 5 | 6 | #[derive(Debug, Clone)] 7 | pub enum Message { 8 | Event(iced_native::Event), 9 | Exit(PathBuf), 10 | Next, 11 | Previous, 12 | Install, 13 | Installed(Result), 14 | Role(&'static [Role]), 15 | PrivateNoiseKey(String), 16 | Network(bitcoin::Network), 17 | DefineStakeholderXpubs(DefineStakeholderXpubs), 18 | DefineManagerXpubs(DefineManagerXpubs), 19 | DefineCpfpDescriptor(DefineCpfpDescriptor), 20 | DefineCoordinator(DefineCoordinator), 21 | DefineEmergencyAddress(String), 22 | DefineCosigners(usize, DefineCosigner), 23 | DefineBitcoind(DefineBitcoind), 24 | } 25 | 26 | #[derive(Debug, Clone)] 27 | pub enum DefineBitcoind { 28 | CookiePathEdited(String), 29 | AddressEdited(String), 30 | } 31 | 32 | #[derive(Debug, Clone)] 33 | pub enum DefineCosigner { 34 | HostEdited(String), 35 | NoiseKeyEdited(String), 36 | } 37 | 38 | #[derive(Debug, Clone)] 39 | pub enum DefineCoordinator { 40 | HostEdited(String), 41 | NoiseKeyEdited(String), 42 | } 43 | 44 | #[derive(Debug, Clone)] 45 | pub enum DefineCpfpDescriptor { 46 | ManagerXpub(usize, String), 47 | } 48 | 49 | #[derive(Debug, Clone)] 50 | pub enum DefineStakeholderXpubs { 51 | OurXpubEdited(String), 52 | StakeholderXpub(usize, ParticipantXpub), 53 | AddXpub, 54 | } 55 | 56 | #[derive(Debug, Clone)] 57 | pub enum DefineManagerXpubs { 58 | ManagersThreshold(Action), 59 | SpendingDelay(Action), 60 | OurXpubEdited(String), 61 | ManagerXpub(usize, ParticipantXpub), 62 | CosignersEnabled(bool), 63 | CosignerKey(usize, String), 64 | AddXpub, 65 | } 66 | 67 | #[derive(Debug, Clone)] 68 | pub enum Action { 69 | Increment, 70 | Decrement, 71 | } 72 | 73 | #[derive(Debug, Clone)] 74 | pub enum ParticipantXpub { 75 | Delete, 76 | XpubEdited(String), 77 | } 78 | -------------------------------------------------------------------------------- /contrib/tools/dummysigner/examples/manager_client.rs: -------------------------------------------------------------------------------- 1 | use futures::prelude::*; 2 | use serde_json::json; 3 | use tokio::net::TcpStream; 4 | use tokio::time::{sleep, Duration}; 5 | use tokio_serde::formats::*; 6 | use tokio_serde::SymmetricallyFramed; 7 | use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec}; 8 | 9 | /// run dummysigner: 10 | /// cargo run -- xprv9yZtssy7SLcqLCQetFHe35ENDKT58vh87MNFYdNkMyphUAcfCXdxLzgG4enc7ZT8NXjBtivtLrtpjZAJzyiTEAKM6NKUeFerP97DZdctJPr 11 | 12 | #[tokio::main] 13 | pub async fn main() { 14 | // Bind a server socket 15 | let mut socket = TcpStream::connect("0.0.0.0:8080").await.unwrap(); 16 | 17 | let (reader, writer) = socket.split(); 18 | 19 | let mut sender = SymmetricallyFramed::new( 20 | FramedWrite::new(writer, LengthDelimitedCodec::new()), 21 | SymmetricalJson::default(), 22 | ); 23 | 24 | let mut receiver = SymmetricallyFramed::new( 25 | FramedRead::new(reader, LengthDelimitedCodec::new()), 26 | SymmetricalJson::::default(), 27 | ); 28 | 29 | sender 30 | .send(json!({ 31 | "spend_tx": "cHNidP8BALQCAAAAAc1946BSKWX5trghNlBq/IIYScLPYqr9Bqs2LfqOYuqcAAAAAAAIAAAAA+BAAAAAAAAAIgAgCOQxrx6W/t0dSZikMBNYG2Yyam/3LIoVrAy6e8ZDUAyA8PoCAAAAACIAIMuwqNTx88KHHtIR0EeURzEu9pUmbnUxd22KzYKi25A2CBH6AgAAAAAiACB18mkXdMgWd4MYRrAoIgDiiLLFlxC1j3Qxg9SSVQfbxQAAAAAAAQEruFn1BQAAAAAiACBI6M9l6zams92tyCK/4gbWyNfJMJzgoOv34L0X7GTovAEDBAEAAAABBWEhAgKTOrEDfq0KpKeFjG1J1nBeH7O8X2awCRive58A7NUmrFGHZHapFHKpXyKvmhuuuFL5qVJy+MIdmPJkiKxrdqkUtsmtuJyMk3Jsg+KhtdlHidd7lWGIrGyTUodnWLJoIgYCApM6sQN+rQqkp4WMbUnWcF4fs7xfZrAJGK97nwDs1SYIJR1gCQAAAAAAIgICUHL04HZXilyJ1B118e1Smr+S8c1qtja46Le7DzMCaUMI+93szQAAAAAAACICAlgt7b9E9GVk5djNsGdTbWDr40zR0YAc/1G7+desKJtDCNZ9f+kAAAAAIgIDRwTey1W1qoj/0e9dBjZiSMExThllURNv8U6ri7pKSQ4IcqlfIgAAAAAA", 32 | })) 33 | .await 34 | .unwrap(); 35 | 36 | if let Some(msg) = receiver.try_next().await.unwrap() { 37 | println!("GOT: {:?}", msg); 38 | } 39 | 40 | sleep(Duration::from_secs(2)).await; 41 | } 42 | -------------------------------------------------------------------------------- /tests/utils/fixtures.rs: -------------------------------------------------------------------------------- 1 | use revaultd::config::Config as DaemonConfig; 2 | 3 | pub fn random_daemon_config() -> DaemonConfig { 4 | toml::from_str( 5 | r#" 6 | coordinator_host = "127.0.0.1:8383" 7 | coordinator_noise_key = "fa4aa4fd8bd5bc2746efff75a9e012305531f41f29557e88cef68e678dffab3a" 8 | daemon = true 9 | data_dir = "/home/edouard/code/revault/demo/demo-noel" 10 | 11 | [bitcoind_config] 12 | addr = "127.0.0.1:9002" 13 | cookie_path = "/home/edouard/code/revault/demo/demo-noel/regtest/bcdir2/regtest/.cookie" 14 | network = "regtest" 15 | 16 | [manager_config] 17 | cosigners = [] 18 | xpub = "tpubD6NzVbkrYhZ4XkehE7ghxNboGmT4Pd1SZ9RWLN5dG5vgRKXQgSxYtsmUgAYsqzdbK9petorBFceU36PNAfkVmrMhfNsJRSoiyWpu6NJA1BQ" 19 | 20 | [scripts_config] 21 | cpfp_descriptor = "wsh(multi(1,tpubD6NzVbkrYhZ4XkehE7ghxNboGmT4Pd1SZ9RWLN5dG5vgRKXQgSxYtsmUgAYsqzdbK9petorBFceU36PNAfkVmrMhfNsJRSoiyWpu6NJA1BQ/*,tpubD6NzVbkrYhZ4XyJXPpnkwCpTazWgerTFgXLtVehbPyoNKVFfPgXRcoxLGupEES1tSteVGsJon85AxEzGyWVSxm8LX8bdZsz87GWt585X2wf/*))#8h972ae2" 22 | deposit_descriptor = "wsh(multi(2,tpubD6NzVbkrYhZ4WmzFjvQrp7sDa4ECUxTi9oby8K4FZkd3XCBtEdKwUiQyYJaxiJo5y42gyDWEczrFpozEjeLxMPxjf2WtkfcbpUdfvNnozWF/*,tpubD6NzVbkrYhZ4XyJXPpnkwCpTazWgerTFgXLtVehbPyoNKVFfPgXRcoxLGupEES1tSteVGsJon85AxEzGyWVSxm8LX8bdZsz87GWt585X2wf/*))#36w5x8qy" 23 | unvault_descriptor = "wsh(andor(multi(1,tpubD6NzVbkrYhZ4XcB3kRJVob8bmjMvA2zBuagidVzh7ASY5FyAEtq4nTzx9wHYu5XDQAg7vdFNiF6yX38kTCK8zjVVmFTiQR2YKAqZBTGjnoD/*,tpubD6NzVbkrYhZ4XkehE7ghxNboGmT4Pd1SZ9RWLN5dG5vgRKXQgSxYtsmUgAYsqzdbK9petorBFceU36PNAfkVmrMhfNsJRSoiyWpu6NJA1BQ/*),older(10),thresh(2,pkh(tpubD6NzVbkrYhZ4WmzFjvQrp7sDa4ECUxTi9oby8K4FZkd3XCBtEdKwUiQyYJaxiJo5y42gyDWEczrFpozEjeLxMPxjf2WtkfcbpUdfvNnozWF/*),a:pkh(tpubD6NzVbkrYhZ4XyJXPpnkwCpTazWgerTFgXLtVehbPyoNKVFfPgXRcoxLGupEES1tSteVGsJon85AxEzGyWVSxm8LX8bdZsz87GWt585X2wf/*))))#lej6yrsc" 24 | 25 | [stakeholder_config] 26 | emergency_address = "bcrt1qqyds0grsuaxpx2dxg4ueugn4p6qyfg6lszmzert77yqh0m8ku3dqxragug" 27 | watchtowers = [] 28 | xpub = "tpubD6NzVbkrYhZ4WmzFjvQrp7sDa4ECUxTi9oby8K4FZkd3XCBtEdKwUiQyYJaxiJo5y42gyDWEczrFpozEjeLxMPxjf2WtkfcbpUdfvNnozWF" 29 | "# 30 | ).unwrap() 31 | } 32 | -------------------------------------------------------------------------------- /src/app/view/deposit.rs: -------------------------------------------------------------------------------- 1 | use iced::{Alignment, Column, Container, Element, Length, QRCode, Row}; 2 | 3 | use revault_ui::component::{button, card, text::Text}; 4 | 5 | use crate::app::{context::Context, error::Error, message::Message, view::layout}; 6 | 7 | /// DepositView is the view rendering the deposit panel. 8 | /// this view is used by the Deposit State. 9 | #[derive(Debug)] 10 | pub struct DepositView { 11 | dashboard: layout::Dashboard, 12 | qr_code: Option, 13 | copy_button: iced::button::State, 14 | } 15 | 16 | impl DepositView { 17 | pub fn new() -> Self { 18 | DepositView { 19 | qr_code: None, 20 | dashboard: layout::Dashboard::default(), 21 | copy_button: iced::button::State::default(), 22 | } 23 | } 24 | 25 | // Address is loaded directly in the view in order to cache the created qrcode. 26 | pub fn load(&mut self, address: &bitcoin::Address) { 27 | self.qr_code = iced::qr_code::State::new(address.to_string()).ok(); 28 | } 29 | 30 | pub fn view<'a>( 31 | &'a mut self, 32 | ctx: &Context, 33 | warning: Option<&Error>, 34 | address: &bitcoin::Address, 35 | ) -> Element<'a, Message> { 36 | let mut col = Column::new() 37 | .align_items(Alignment::Center) 38 | .spacing(20) 39 | .push(Text::new("Please, use this deposit address:").bold()); 40 | 41 | if let Some(qr_code) = self.qr_code.as_mut() { 42 | col = col.push(Container::new(QRCode::new(qr_code).cell_size(5))); 43 | } 44 | 45 | let addr = address.to_string(); 46 | col = col.push(Container::new( 47 | Row::new() 48 | .push(Container::new(Text::new(&addr).small())) 49 | .push( 50 | button::clipboard(&mut self.copy_button, Message::Clipboard(addr)) 51 | .width(Length::Shrink), 52 | ) 53 | .align_items(Alignment::Center), 54 | )); 55 | 56 | self.dashboard.view(ctx, warning, card::white(col)) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # revault-gui 2 | 3 | Revault GUI is an user graphical interface written in rust for the 4 | [Revault daemon](https://github.com/revault/revaultd). 5 | 6 | stakeholder-regtest 7 | manager-regtest 8 | 9 | ## Dependencies 10 | 11 | - `fontconfig` (On Debian/Ubuntu `apt install libfontconfig1-dev`) 12 | - [`pkg-config`](https://www.freedesktop.org/wiki/Software/pkg-config/) (On Debian/Ubuntu `apt install pkg-config`) 13 | - [`libxkbcommon`](https://xkbcommon.org/) for the dummy signer (On Debian/Ubuntu `apt install libxkbcommon-dev`) 14 | - Vulkan drivers (On Debian/Ubuntu `apt install mesa-vulkan-drivers libvulkan-dev`) 15 | - `libudev-dev` (On Debian/Ubuntu `apt install libudev-dev`) 16 | 17 | We are striving to remove dependencies, especially the 3D ones. 18 | 19 | ## Usage 20 | 21 | `revault-gui --datadir --` 22 | 23 | The default `datadir` is the default `revaultd` `datadir` (`~/.revault` 24 | for linux) and the default `network` is the bitcoin mainnet. 25 | 26 | If no argument is provided, the GUI checks in the default `datadir` 27 | the configuration file for the bitcoin mainnet. 28 | 29 | If the provided `datadir` is empty or does not have the configuration 30 | file for the targeted `network`, the GUI starts with the installer mode. 31 | 32 | Instead of using `--datadir` and `--`, a direct path to 33 | the GUI configuration file can be provided with `--conf`. 34 | 35 | After start up, The GUI will connect to the running revaultd. 36 | A command starting revaultd is launched if no connection is made. 37 | 38 | ## Get started 39 | 40 | See [aquarium](https://github.com/revault/aquarium) for trying out a 41 | Revault deployment in no time. 42 | 43 | See [doc/DEMO_TESTNET.md](doc/DEMO_TESTNET.md) for instructions on how 44 | to setup Revault on testnet (more involved and likely needs more 45 | participants). 46 | 47 | ## Troubleshooting 48 | 49 | - If you encounter layout issue on `X11`, try to start the GUI with 50 | `WINIT_X11_SCALE_FACTOR` manually set to 1 51 | -------------------------------------------------------------------------------- /src/app/error.rs: -------------------------------------------------------------------------------- 1 | use crate::daemon::RevaultDError; 2 | use revaultd::config::ConfigError; 3 | use std::convert::From; 4 | use std::io::ErrorKind; 5 | 6 | #[derive(Debug, Clone)] 7 | pub enum Error { 8 | Hardware(revault_hwi::HWIError), 9 | // TODO: add Clone to ConfigError 10 | Config(String), 11 | Daemon(RevaultDError), 12 | Unexpected(String), 13 | } 14 | 15 | impl std::fmt::Display for Error { 16 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 17 | match self { 18 | Self::Config(e) => write!(f, "{}", e), 19 | Self::Hardware(e) => write!(f, "{}", e), 20 | Self::Daemon(e) => match e { 21 | RevaultDError::Unexpected(e) => write!(f, "{}", e), 22 | RevaultDError::NoAnswer => write!(f, "Daemon did not answer"), 23 | RevaultDError::Transport(Some(ErrorKind::ConnectionRefused), _) => { 24 | write!(f, "Failed to connect to daemon") 25 | } 26 | RevaultDError::Transport(kind, e) => { 27 | if let Some(k) = kind { 28 | write!(f, "{} [{:?}]", e, k) 29 | } else { 30 | write!(f, "{}", e) 31 | } 32 | } 33 | RevaultDError::Start(e) => { 34 | write!(f, "Failed to start daemon: {}", e) 35 | } 36 | RevaultDError::Rpc(code, e) => { 37 | write!(f, "[{:?}] {}", code, e) 38 | } 39 | }, 40 | Self::Unexpected(e) => write!(f, "Unexpected error: {}", e), 41 | } 42 | } 43 | } 44 | 45 | impl From for Error { 46 | fn from(error: ConfigError) -> Self { 47 | Error::Config(error.to_string()) 48 | } 49 | } 50 | 51 | impl From for Error { 52 | fn from(error: RevaultDError) -> Self { 53 | Error::Daemon(error) 54 | } 55 | } 56 | 57 | impl From for Error { 58 | fn from(error: revault_hwi::HWIError) -> Self { 59 | Error::Hardware(error) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /ui/src/color.rs: -------------------------------------------------------------------------------- 1 | use iced::Color; 2 | 3 | pub const BACKGROUND: Color = Color::from_rgb( 4 | 0xF6 as f32 / 255.0, 5 | 0xF7 as f32 / 255.0, 6 | 0xF8 as f32 / 255.0, 7 | ); 8 | 9 | pub const FOREGROUND: Color = Color::WHITE; 10 | 11 | pub const SECONDARY: Color = Color::from_rgb( 12 | 0xe1 as f32 / 255.0, 13 | 0xe4 as f32 / 255.0, 14 | 0xe8 as f32 / 255.0, 15 | ); 16 | 17 | pub const PRIMARY: Color = Color::from_rgb( 18 | 0xF0 as f32 / 255.0, 19 | 0x43 as f32 / 255.0, 20 | 0x59 as f32 / 255.0, 21 | ); 22 | 23 | pub const PRIMARY_LIGHT: Color = Color::from_rgba( 24 | 0xF0 as f32 / 255.0, 25 | 0x43 as f32 / 255.0, 26 | 0x59 as f32 / 255.0, 27 | 0.5f32, 28 | ); 29 | 30 | pub const SUCCESS: Color = Color::from_rgb( 31 | 0x29 as f32 / 255.0, 32 | 0xBC as f32 / 255.0, 33 | 0x97 as f32 / 255.0, 34 | ); 35 | 36 | #[allow(dead_code)] 37 | pub const SUCCESS_LIGHT: Color = Color::from_rgba( 38 | 0x29 as f32 / 255.0, 39 | 0xBC as f32 / 255.0, 40 | 0x97 as f32 / 255.0, 41 | 0.5f32, 42 | ); 43 | 44 | pub const ALERT: Color = Color::from_rgb( 45 | 0xF0 as f32 / 255.0, 46 | 0x43 as f32 / 255.0, 47 | 0x59 as f32 / 255.0, 48 | ); 49 | 50 | pub const ALERT_LIGHT: Color = Color::from_rgba( 51 | 0xF0 as f32 / 255.0, 52 | 0x43 as f32 / 255.0, 53 | 0x59 as f32 / 255.0, 54 | 0.5f32, 55 | ); 56 | 57 | pub const WARNING: Color = 58 | Color::from_rgb(0xFF as f32 / 255.0, 0xa7 as f32 / 255.0, 0x0 as f32 / 255.0); 59 | 60 | pub const WARNING_LIGHT: Color = Color::from_rgba( 61 | 0xFF as f32 / 255.0, 62 | 0xa7 as f32 / 255.0, 63 | 0x0 as f32 / 255.0, 64 | 0.5f32, 65 | ); 66 | 67 | pub const CANCEL: Color = Color::from_rgb( 68 | 0x34 as f32 / 255.0, 69 | 0x37 as f32 / 255.0, 70 | 0x3D as f32 / 255.0, 71 | ); 72 | 73 | pub const INFO: Color = Color::from_rgb( 74 | 0x2A as f32 / 255.0, 75 | 0x98 as f32 / 255.0, 76 | 0xBD as f32 / 255.0, 77 | ); 78 | 79 | pub const INFO_LIGHT: Color = Color::from_rgba( 80 | 0x2A as f32 / 255.0, 81 | 0x98 as f32 / 255.0, 82 | 0xBD as f32 / 255.0, 83 | 0.5f32, 84 | ); 85 | 86 | pub const DARK_GREY: Color = Color::from_rgb( 87 | 0x8c as f32 / 255.0, 88 | 0x97 as f32 / 255.0, 89 | 0xa6 as f32 / 255.0, 90 | ); 91 | -------------------------------------------------------------------------------- /src/app/view/sign.rs: -------------------------------------------------------------------------------- 1 | use iced::{Alignment, Column, Container, Element, Length}; 2 | 3 | use revault_ui::{ 4 | component::{button, card, text::Text}, 5 | icon, 6 | }; 7 | 8 | use crate::app::{context::Context, message::SignMessage}; 9 | 10 | #[derive(Debug)] 11 | pub struct SignerView { 12 | sign_button: iced::button::State, 13 | } 14 | 15 | impl SignerView { 16 | pub fn new() -> Self { 17 | SignerView { 18 | sign_button: iced::button::State::default(), 19 | } 20 | } 21 | 22 | pub fn view( 23 | &mut self, 24 | _ctx: &Context, 25 | connected: bool, 26 | processing: bool, 27 | signed: bool, 28 | ) -> Element { 29 | if signed { 30 | return card::success(Container::new( 31 | Column::new() 32 | .align_items(Alignment::Center) 33 | .spacing(20) 34 | .push(Text::from(icon::done_icon()).size(20).success()) 35 | .push(Text::new("Signed").success()), 36 | )) 37 | .padding(50) 38 | .width(Length::Fill) 39 | .center_x() 40 | .into(); 41 | } 42 | if connected { 43 | let mut sign_button = button::primary( 44 | &mut self.sign_button, 45 | button::button_content(None, " Sign ").width(Length::Units(200)), 46 | ); 47 | if !processing { 48 | sign_button = sign_button.on_press(SignMessage::SelectSign); 49 | } 50 | card::white(Container::new( 51 | Column::new() 52 | .align_items(Alignment::Center) 53 | .spacing(20) 54 | .push(icon::connected_device_icon().size(20)) 55 | .push(sign_button), 56 | )) 57 | .padding(50) 58 | .width(Length::Fill) 59 | .center_x() 60 | .into() 61 | } else { 62 | card::white(Container::new( 63 | Column::new() 64 | .align_items(Alignment::Center) 65 | .spacing(20) 66 | .push(icon::connect_device_icon().size(20)) 67 | .push(Text::new("Connect hardware wallet")), 68 | )) 69 | .padding(50) 70 | .width(Length::Fill) 71 | .center_x() 72 | .into() 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /contrib/tools/dummysigner/examples/stakeholder_batch.rs: -------------------------------------------------------------------------------- 1 | use futures::prelude::*; 2 | use serde_json::json; 3 | use tokio::net::TcpStream; 4 | use tokio::time::{sleep, Duration}; 5 | use tokio_serde::formats::*; 6 | use tokio_serde::SymmetricallyFramed; 7 | use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec}; 8 | 9 | /// run dummysigner: 10 | /// cargo run -- --conf examples/examples_cfg.toml 11 | 12 | #[tokio::main] 13 | pub async fn main() { 14 | // Bind a server socket 15 | let mut socket = TcpStream::connect("0.0.0.0:8080").await.unwrap(); 16 | 17 | let (reader, writer) = socket.split(); 18 | 19 | let mut sender = SymmetricallyFramed::new( 20 | FramedWrite::new(writer, LengthDelimitedCodec::new()), 21 | SymmetricalJson::default(), 22 | ); 23 | 24 | let mut receiver = SymmetricallyFramed::new( 25 | FramedRead::new(reader, LengthDelimitedCodec::new()), 26 | SymmetricalJson::::default(), 27 | ); 28 | 29 | // Secure deposits 30 | sender 31 | .send(json!({ 32 | "deposits": [ 33 | { 34 | "outpoint": "899aecbc9a3b06feaf096fc576b35da352a1ca1aa0c34db23ccfa944f30fae47:1", 35 | "amount": 120000000, 36 | "derivation_index": 0, 37 | }, 38 | { 39 | "outpoint": "f72c10cfb1e915c18f35aa7f658bea0fe75188e93abfcc13dae3359bd311caed:0", 40 | "amount": 43000000, 41 | "derivation_index": 1, 42 | } 43 | ] 44 | })) 45 | .await 46 | .unwrap(); 47 | 48 | if let Some(msg) = receiver.try_next().await.unwrap() { 49 | println!("GOT: {:?}", msg); 50 | } 51 | 52 | sleep(Duration::from_secs(2)).await; 53 | 54 | // Delegate vaults 55 | sender 56 | .send(json!({ 57 | "vaults": [ 58 | { 59 | "outpoint": "899aecbc9a3b06feaf096fc576b35da352a1ca1aa0c34db23ccfa944f30fae47:1", 60 | "amount": 120000000, 61 | "derivation_index": 0, 62 | }, 63 | { 64 | "outpoint": "f72c10cfb1e915c18f35aa7f658bea0fe75188e93abfcc13dae3359bd311caed:0", 65 | "amount": 43000000, 66 | "derivation_index": 1, 67 | } 68 | ] 69 | })) 70 | .await 71 | .unwrap(); 72 | 73 | if let Some(msg) = receiver.try_next().await.unwrap() { 74 | println!("GOT: {:?}", msg); 75 | } 76 | 77 | sleep(Duration::from_secs(2)).await; 78 | } 79 | -------------------------------------------------------------------------------- /tests/utils/mock.rs: -------------------------------------------------------------------------------- 1 | use revault_gui::daemon::{client::Client, RevaultDError}; 2 | use serde::{de::DeserializeOwned, Serialize}; 3 | use serde_json::{json, Value}; 4 | use std::fmt::Debug; 5 | use std::sync::{ 6 | mpsc::{channel, Receiver, Sender}, 7 | Mutex, 8 | }; 9 | use std::thread; 10 | 11 | #[derive(Debug)] 12 | pub struct DaemonClient { 13 | transport: Mutex<(Sender, Receiver>)>, 14 | } 15 | 16 | impl Client for DaemonClient { 17 | type Error = RevaultDError; 18 | fn request( 19 | &self, 20 | method: &str, 21 | params: Option, 22 | ) -> Result { 23 | let req = json!({"method": method, "params": params}); 24 | let connection = self.transport.lock().expect("Failed to unlock"); 25 | connection 26 | .0 27 | .send(req) 28 | .expect("Mock client failed to send request"); 29 | connection 30 | .1 31 | .recv() 32 | .expect("Mock client failed to receive response") 33 | .map(|value| serde_json::from_value(value).unwrap()) 34 | } 35 | } 36 | 37 | pub struct Daemon { 38 | requests: Vec<(Option, Result)>, 39 | } 40 | 41 | impl Daemon { 42 | pub fn new(requests: Vec<(Option, Result)>) -> Self { 43 | Self { requests } 44 | } 45 | 46 | pub fn run(self) -> DaemonClient { 47 | let (client_sender, daemon_receiver) = channel(); 48 | let (daemon_sender, client_receiver) = channel(); 49 | 50 | thread::spawn(move || { 51 | let mut requests = self.requests.into_iter(); 52 | while let Ok(msg) = daemon_receiver.recv() { 53 | let request = requests 54 | .next() 55 | .expect("Mock Daemon must have all requests mocked in the right order"); 56 | if let Some(body) = request.0 { 57 | assert_eq!(body, msg); 58 | } 59 | daemon_sender 60 | .send(request.1) 61 | .expect("Mock daemon failed to send response") 62 | } 63 | // close the daemon -> client channel after 64 | // the client -> daemon channel is closed. 65 | // (client -> daemon channel is closed when DaemonClient is dropped) 66 | drop(daemon_sender); 67 | // Readable with `cargo test -- --nocapture` 68 | println!("The daemon has stopped!"); 69 | }); 70 | 71 | DaemonClient { 72 | transport: Mutex::new((client_sender, client_receiver)), 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /contrib/tools/dummysigner/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::str::FromStr; 3 | 4 | use serde::{de, Deserialize, Deserializer}; 5 | 6 | use revault_tx::{ 7 | bitcoin::util::bip32::ExtendedPrivKey, 8 | scripts::{CpfpDescriptor, DepositDescriptor, EmergencyAddress, UnvaultDescriptor}, 9 | }; 10 | 11 | #[derive(Debug, Deserialize)] 12 | pub struct Config { 13 | pub keys: Vec, 14 | pub descriptors: Option, 15 | pub emergency_address: Option, 16 | } 17 | 18 | #[derive(Debug, Deserialize)] 19 | pub struct Key { 20 | pub name: String, 21 | #[serde(deserialize_with = "deserialize_fromstr")] 22 | pub xpriv: ExtendedPrivKey, 23 | } 24 | 25 | #[derive(Debug, Deserialize)] 26 | pub struct Descriptors { 27 | #[serde(deserialize_with = "deserialize_fromstr")] 28 | pub deposit_descriptor: DepositDescriptor, 29 | #[serde(deserialize_with = "deserialize_fromstr")] 30 | pub unvault_descriptor: UnvaultDescriptor, 31 | #[serde(deserialize_with = "deserialize_fromstr")] 32 | pub cpfp_descriptor: CpfpDescriptor, 33 | } 34 | 35 | impl Config { 36 | pub fn new(xprivs: Vec) -> Self { 37 | Self { 38 | keys: xprivs 39 | .into_iter() 40 | .map(|xpriv| Key { 41 | name: "".to_string(), 42 | xpriv, 43 | }) 44 | .collect(), 45 | descriptors: None, 46 | emergency_address: None, 47 | } 48 | } 49 | pub fn from_file(path: &Path) -> Result { 50 | std::fs::read(path) 51 | .map_err(|e| match e.kind() { 52 | std::io::ErrorKind::NotFound => ConfigError::NotFound, 53 | _ => ConfigError::ReadingFile(format!("Reading configuration file: {}", e)), 54 | }) 55 | .and_then(|file_content| { 56 | toml::from_slice::(&file_content).map_err(|e| { 57 | ConfigError::ReadingFile(format!("Parsing configuration file: {}", e)) 58 | }) 59 | }) 60 | } 61 | } 62 | 63 | #[derive(PartialEq, Eq, Debug, Clone)] 64 | pub enum ConfigError { 65 | NotFound, 66 | ReadingFile(String), 67 | } 68 | 69 | impl std::fmt::Display for ConfigError { 70 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 71 | match self { 72 | Self::NotFound => write!(f, "Config file not found"), 73 | Self::ReadingFile(e) => write!(f, "Error while reading file: {}", e), 74 | } 75 | } 76 | } 77 | 78 | fn deserialize_fromstr<'de, D, T>(deserializer: D) -> Result 79 | where 80 | D: Deserializer<'de>, 81 | T: FromStr, 82 | ::Err: std::fmt::Display, 83 | { 84 | let string = String::deserialize(deserializer)?; 85 | T::from_str(&string) 86 | .map_err(|e| de::Error::custom(format!("Error parsing descriptor '{}': '{}'", string, e))) 87 | } 88 | -------------------------------------------------------------------------------- /src/installer/config.rs: -------------------------------------------------------------------------------- 1 | use bitcoin::{util::bip32, Network}; 2 | use revaultd::config::{BitcoindConfig, ManagerConfig, WatchtowerConfig}; 3 | 4 | use serde::Serialize; 5 | use std::{net::SocketAddr, path::PathBuf, time::Duration}; 6 | 7 | /// If we are a stakeholder, we need to connect to our watchtower(s) 8 | #[derive(Debug, Clone, Serialize)] 9 | pub struct StakeholderConfig { 10 | pub xpub: bip32::ExtendedPubKey, 11 | pub watchtowers: Vec, 12 | pub emergency_address: String, 13 | } 14 | 15 | #[derive(Debug, Clone, Serialize)] 16 | pub struct ScriptsConfig { 17 | pub deposit_descriptor: String, 18 | pub unvault_descriptor: String, 19 | pub cpfp_descriptor: String, 20 | } 21 | 22 | /// Static informations we require to operate 23 | #[derive(Debug, Clone, Serialize)] 24 | pub struct Config { 25 | /// Everything we need to know to talk to bitcoind 26 | pub bitcoind_config: BitcoindConfig, 27 | /// Some() if we are a stakeholder 28 | pub stakeholder_config: Option, 29 | /// Some() if we are a manager 30 | pub manager_config: Option, 31 | /// Descriptors 32 | pub scripts_config: ScriptsConfig, 33 | /// The host of the sync server (may be an IP or a hidden service) 34 | pub coordinator_host: String, 35 | /// The Noise static public key of the sync server 36 | pub coordinator_noise_key: String, 37 | /// The poll intervals for signature fetching (default: 1min) 38 | pub coordinator_poll_seconds: Option, 39 | /// An optional custom data directory 40 | pub data_dir: Option, 41 | /// Whether to daemonize the process 42 | pub daemon: Option, 43 | /// What messages to log 44 | pub log_level: Option, 45 | } 46 | 47 | impl Config { 48 | pub const DEFAULT_FILE_NAME: &'static str = "revaultd.toml"; 49 | /// returns a revaultd config with empty or dummy values 50 | pub fn new() -> Config { 51 | Self { 52 | bitcoind_config: BitcoindConfig { 53 | network: Network::Bitcoin, 54 | cookie_path: PathBuf::new(), 55 | addr: SocketAddr::new( 56 | std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)), 57 | 8080, 58 | ), 59 | poll_interval_secs: Duration::from_secs(30), 60 | }, 61 | stakeholder_config: None, 62 | manager_config: None, 63 | scripts_config: ScriptsConfig { 64 | deposit_descriptor: "".to_string(), 65 | unvault_descriptor: "".to_string(), 66 | cpfp_descriptor: "".to_string(), 67 | }, 68 | coordinator_host: "".to_string(), 69 | coordinator_noise_key: "".to_string(), 70 | coordinator_poll_seconds: None, 71 | data_dir: None, 72 | daemon: None, 73 | log_level: None, 74 | } 75 | } 76 | } 77 | 78 | impl Default for Config { 79 | fn default() -> Self { 80 | Self::new() 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/daemon/model.rs: -------------------------------------------------------------------------------- 1 | use bitcoin::{consensus::encode, hashes::hex::FromHex, OutPoint, Transaction}; 2 | 3 | pub use revaultd::commands::{ 4 | GetInfoResult, HistoryEvent, HistoryEventKind, ListOnchainTxEntry, ListSpendEntry, 5 | ListSpendStatus, RevocationTransactions, ServerStatus, ServersStatuses, VaultStatus, 6 | WalletTransaction, 7 | }; 8 | use revaultd::commands::{ListPresignedTxEntry, ListVaultsEntry}; 9 | 10 | pub type Vault = ListVaultsEntry; 11 | 12 | pub fn outpoint(vault: &Vault) -> OutPoint { 13 | OutPoint::new(vault.txid, vault.vout) 14 | } 15 | 16 | pub const DEPOSIT_AND_CURRENT_VAULT_STATUSES: [VaultStatus; 11] = [ 17 | VaultStatus::Funded, 18 | VaultStatus::Securing, 19 | VaultStatus::Secured, 20 | VaultStatus::Activating, 21 | VaultStatus::Active, 22 | VaultStatus::Unvaulting, 23 | VaultStatus::Unvaulted, 24 | VaultStatus::Canceling, 25 | VaultStatus::EmergencyVaulting, 26 | VaultStatus::UnvaultEmergencyVaulting, 27 | VaultStatus::Spending, 28 | ]; 29 | 30 | pub const CURRENT_VAULT_STATUSES: [VaultStatus; 10] = [ 31 | VaultStatus::Securing, 32 | VaultStatus::Secured, 33 | VaultStatus::Activating, 34 | VaultStatus::Active, 35 | VaultStatus::Unvaulting, 36 | VaultStatus::Unvaulted, 37 | VaultStatus::Canceling, 38 | VaultStatus::EmergencyVaulting, 39 | VaultStatus::UnvaultEmergencyVaulting, 40 | VaultStatus::Spending, 41 | ]; 42 | 43 | pub const ACTIVE_VAULT_STATUSES: [VaultStatus; 1] = [VaultStatus::Active]; 44 | 45 | pub const INACTIVE_VAULT_STATUSES: [VaultStatus; 4] = [ 46 | VaultStatus::Funded, 47 | VaultStatus::Securing, 48 | VaultStatus::Secured, 49 | VaultStatus::Activating, 50 | ]; 51 | 52 | pub const MOVING_VAULT_STATUSES: [VaultStatus; 6] = [ 53 | VaultStatus::Unvaulting, 54 | VaultStatus::Unvaulted, 55 | VaultStatus::Canceling, 56 | VaultStatus::EmergencyVaulting, 57 | VaultStatus::UnvaultEmergencyVaulting, 58 | VaultStatus::Spending, 59 | ]; 60 | 61 | pub const MOVED_VAULT_STATUSES: [VaultStatus; 4] = [ 62 | VaultStatus::Canceled, 63 | VaultStatus::EmergencyVaulted, 64 | VaultStatus::UnvaultEmergencyVaulted, 65 | VaultStatus::Spent, 66 | ]; 67 | 68 | pub type SpendTxStatus = ListSpendStatus; 69 | 70 | pub const ALL_SPEND_TX_STATUSES: [SpendTxStatus; 5] = [ 71 | SpendTxStatus::NonFinal, 72 | SpendTxStatus::Pending, 73 | SpendTxStatus::Broadcasted, 74 | SpendTxStatus::Confirmed, 75 | SpendTxStatus::Deprecated, 76 | ]; 77 | 78 | pub const PROCESSING_SPEND_TX_STATUSES: [SpendTxStatus; 2] = 79 | [SpendTxStatus::Pending, SpendTxStatus::Broadcasted]; 80 | 81 | pub type VaultTransactions = ListOnchainTxEntry; 82 | pub type VaultPresignedTransactions = ListPresignedTxEntry; 83 | 84 | pub fn transaction_from_hex(hex: &str) -> Transaction { 85 | let bytes = Vec::from_hex(&hex).unwrap(); 86 | encode::deserialize::(&bytes).unwrap() 87 | } 88 | 89 | pub type SpendTx = ListSpendEntry; 90 | 91 | pub const ALL_HISTORY_EVENTS: [HistoryEventKind; 3] = [ 92 | HistoryEventKind::Cancel, 93 | HistoryEventKind::Deposit, 94 | HistoryEventKind::Spend, 95 | ]; 96 | -------------------------------------------------------------------------------- /hwi/src/app/revault.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | 3 | use bitcoin::{ 4 | blockdata::transaction::OutPoint, util::psbt::PartiallySignedTransaction as Psbt, Amount, 5 | }; 6 | 7 | use crate::{HWIError, HWI}; 8 | 9 | /// RevaultHWI is the common Revault Hardware Wallet Interface. 10 | #[async_trait] 11 | pub trait RevaultHWI: HWI { 12 | /// Returns true if the device is able to secure and delegate vaults 13 | /// by creating and signing itself the revocation transactions and the 14 | /// unvault transaction from internal descriptors. 15 | async fn has_revault_app(&mut self) -> bool; 16 | 17 | /// Sign the revocation transactions. 18 | async fn sign_revocation_txs( 19 | &mut self, 20 | emergency_tx: &Psbt, 21 | emergency_unvault_tx: &Psbt, 22 | cancel_tx: &[Psbt; 5], 23 | ) -> Result<(Psbt, Psbt, [Psbt; 5]), HWIError>; 24 | 25 | /// Sign the unvault transaction required for delegation. 26 | async fn sign_unvault_tx(&mut self, unvault_tx: &Psbt) -> Result; 27 | 28 | /// Create vaults from deposits by giving the utxos to the hardware wallet storing the 29 | /// descriptors and deriving itself the revocation transactions. 30 | async fn create_vaults( 31 | &mut self, 32 | deposits: &[(OutPoint, Amount, u32)], 33 | ) -> Result, HWIError>; 34 | 35 | /// Delegate a list of vaults by giving the utxos to an hardware wallet storing the 36 | /// descriptors and deriving itself the unvault transactions. 37 | async fn delegate_vaults( 38 | &mut self, 39 | vaults: &[(OutPoint, Amount, u32)], 40 | ) -> Result, HWIError>; 41 | } 42 | 43 | pub trait NoRevaultApp {} 44 | 45 | #[async_trait] 46 | impl RevaultHWI for T { 47 | async fn has_revault_app(&mut self) -> bool { 48 | false 49 | } 50 | 51 | async fn sign_revocation_txs( 52 | &mut self, 53 | emergency_tx: &Psbt, 54 | emergency_unvault_tx: &Psbt, 55 | cancel_txs: &[Psbt; 5], 56 | ) -> Result<(Psbt, Psbt, [Psbt; 5]), HWIError> { 57 | let emergency_tx = self.sign_tx(emergency_tx).await?; 58 | let emergency_unvault_tx = self.sign_tx(emergency_unvault_tx).await?; 59 | let cancel_txs = [ 60 | self.sign_tx(&cancel_txs[0]).await?, 61 | self.sign_tx(&cancel_txs[1]).await?, 62 | self.sign_tx(&cancel_txs[2]).await?, 63 | self.sign_tx(&cancel_txs[3]).await?, 64 | self.sign_tx(&cancel_txs[4]).await?, 65 | ]; 66 | Ok((emergency_tx, emergency_unvault_tx, cancel_txs)) 67 | } 68 | 69 | async fn sign_unvault_tx(&mut self, unvault_tx: &Psbt) -> Result { 70 | self.sign_tx(unvault_tx).await 71 | } 72 | 73 | async fn create_vaults( 74 | &mut self, 75 | _deposits: &[(OutPoint, Amount, u32)], 76 | ) -> Result, HWIError> { 77 | Err(HWIError::UnimplementedMethod) 78 | } 79 | 80 | async fn delegate_vaults( 81 | &mut self, 82 | _vaults: &[(OutPoint, Amount, u32)], 83 | ) -> Result, HWIError> { 84 | Err(HWIError::UnimplementedMethod) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/app/state/cmd.rs: -------------------------------------------------------------------------------- 1 | use bitcoin::{util::psbt::PartiallySignedTransaction as Psbt, OutPoint, Txid}; 2 | use std::sync::Arc; 3 | 4 | use crate::daemon::{ 5 | model::{ 6 | RevocationTransactions, ServersStatuses, SpendTx, SpendTxStatus, Vault, VaultStatus, 7 | VaultTransactions, 8 | }, 9 | Daemon, RevaultDError, 10 | }; 11 | 12 | /// retrieves a bitcoin address for deposit. 13 | pub async fn get_deposit_address( 14 | revaultd: Arc, 15 | ) -> Result { 16 | revaultd.get_deposit_address() 17 | } 18 | 19 | pub async fn list_vaults( 20 | revaultd: Arc, 21 | statuses: Option<&[VaultStatus]>, 22 | outpoints: Option>, 23 | ) -> Result, RevaultDError> { 24 | revaultd.list_vaults(statuses, outpoints.as_deref()) 25 | } 26 | 27 | pub async fn get_onchain_txs( 28 | revaultd: Arc, 29 | outpoint: OutPoint, 30 | ) -> Result { 31 | let list = revaultd.list_onchain_transactions(&[outpoint])?; 32 | if list.is_empty() { 33 | return Err(RevaultDError::Unexpected( 34 | "vault has no onchain_transactions".to_string(), 35 | )); 36 | } 37 | 38 | Ok(list[0].to_owned()) 39 | } 40 | 41 | pub async fn get_revocation_txs( 42 | revaultd: Arc, 43 | outpoint: OutPoint, 44 | ) -> Result { 45 | revaultd.get_revocation_txs(&outpoint) 46 | } 47 | 48 | pub async fn get_unvault_tx( 49 | revaultd: Arc, 50 | outpoint: OutPoint, 51 | ) -> Result { 52 | revaultd.get_unvault_tx(&outpoint) 53 | } 54 | 55 | pub async fn set_unvault_tx( 56 | revaultd: Arc, 57 | outpoint: OutPoint, 58 | unvault_tx: Psbt, 59 | ) -> Result<(), RevaultDError> { 60 | revaultd.set_unvault_tx(&outpoint, &unvault_tx) 61 | } 62 | 63 | pub async fn update_spend_tx( 64 | revaultd: Arc, 65 | psbt: Psbt, 66 | ) -> Result<(), RevaultDError> { 67 | revaultd.update_spend_tx(&psbt) 68 | } 69 | 70 | pub async fn list_spend_txs( 71 | revaultd: Arc, 72 | statuses: Option<&[SpendTxStatus]>, 73 | ) -> Result, RevaultDError> { 74 | revaultd.list_spend_txs(statuses) 75 | } 76 | 77 | pub async fn delete_spend_tx( 78 | revaultd: Arc, 79 | txid: Txid, 80 | ) -> Result<(), RevaultDError> { 81 | revaultd.delete_spend_tx(&txid) 82 | } 83 | 84 | pub async fn broadcast_spend_tx( 85 | revaultd: Arc, 86 | txid: Txid, 87 | with_priority: bool, 88 | ) -> Result<(), RevaultDError> { 89 | revaultd.broadcast_spend_tx(&txid, with_priority) 90 | } 91 | 92 | pub async fn emergency(revaultd: Arc) -> Result<(), RevaultDError> { 93 | revaultd.emergency() 94 | } 95 | 96 | pub async fn get_server_status( 97 | revaultd: Arc, 98 | ) -> Result { 99 | revaultd.get_server_status() 100 | } 101 | -------------------------------------------------------------------------------- /src/app/state/vault.rs: -------------------------------------------------------------------------------- 1 | use iced::{Command, Element}; 2 | use std::sync::Arc; 3 | 4 | use crate::{ 5 | app::{ 6 | context::Context, 7 | error::Error, 8 | message::{Message, VaultMessage}, 9 | state::cmd::get_onchain_txs, 10 | view::{ 11 | vault::{VaultModal, VaultView}, 12 | LoadingModal, 13 | }, 14 | }, 15 | daemon::{ 16 | model::{self, outpoint, VaultTransactions}, 17 | Daemon, 18 | }, 19 | }; 20 | 21 | #[derive(Debug)] 22 | pub struct VaultListItem { 23 | pub vault: model::Vault, 24 | view: T, 25 | } 26 | 27 | impl VaultListItem { 28 | pub fn new(vault: model::Vault) -> Self { 29 | Self { 30 | vault, 31 | view: T::new(), 32 | } 33 | } 34 | 35 | pub fn view(&mut self, ctx: &Context) -> Element { 36 | self.view.view(ctx, &self.vault) 37 | } 38 | } 39 | 40 | /// SelectedVault is a widget displaying information of a vault 41 | /// and handling user action on it. 42 | #[derive(Debug)] 43 | pub enum Vault { 44 | Loading { 45 | vault: model::Vault, 46 | fail: Option, 47 | view: LoadingModal, 48 | }, 49 | Loaded { 50 | txs: VaultTransactions, 51 | vault: model::Vault, 52 | view: VaultModal, 53 | }, 54 | } 55 | 56 | impl Vault { 57 | pub fn new(vault: model::Vault) -> Self { 58 | Self::Loading { 59 | vault, 60 | view: LoadingModal::default(), 61 | fail: None, 62 | } 63 | } 64 | 65 | pub fn inner(&self) -> &model::Vault { 66 | match self { 67 | Self::Loading { vault, .. } => vault, 68 | Self::Loaded { vault, .. } => vault, 69 | } 70 | } 71 | 72 | pub fn update(&mut self, _ctx: &Context, message: VaultMessage) -> Command { 73 | if let Self::Loading { fail, vault, .. } = self { 74 | if let VaultMessage::OnChainTransactions(res) = message { 75 | match res { 76 | Ok(txs) => { 77 | *self = Self::Loaded { 78 | vault: vault.clone(), 79 | txs, 80 | view: VaultModal::new(), 81 | } 82 | } 83 | Err(e) => *fail = Some(e.into()), 84 | } 85 | } 86 | } 87 | Command::none() 88 | } 89 | 90 | pub fn view(&mut self, ctx: &Context) -> Element { 91 | match self { 92 | Self::Loading { view, fail, .. } => view.view(ctx, fail.as_ref(), Message::Close), 93 | Self::Loaded { view, vault, txs } => view.view(ctx, vault, &txs), 94 | } 95 | } 96 | 97 | pub fn load(&self, revaultd: Arc) -> Command { 98 | if let Self::Loading { vault, .. } = self { 99 | Command::perform( 100 | get_onchain_txs(revaultd, outpoint(&vault)), 101 | VaultMessage::OnChainTransactions, 102 | ) 103 | } else { 104 | Command::none() 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/daemon/client/error.rs: -------------------------------------------------------------------------------- 1 | // Rust JSON-RPC Library 2 | // Written in 2015 by 3 | // Andrew Poelstra 4 | // 5 | // To the extent possible under law, the author(s) have dedicated all 6 | // copyright and related and neighboring rights to this software to 7 | // the public domain worldwide. This software is distributed without 8 | // any warranty. 9 | // 10 | // You should have received a copy of the CC0 Public Domain Dedication 11 | // along with this software. 12 | // If not, see . 13 | // 14 | 15 | //! Error handling 16 | //! 17 | //! Some useful methods for creating Error objects 18 | //! 19 | 20 | use std::io; 21 | use std::{error, fmt}; 22 | 23 | use serde::{Deserialize, Serialize}; 24 | 25 | #[allow(dead_code)] 26 | #[derive(Debug)] 27 | #[allow(non_camel_case_types)] 28 | pub enum RpcErrorCode { 29 | // Standard errors defined by JSON-RPC 2.0 standard 30 | /// Invalid request 31 | JSONRPC2_INVALID_REQUEST = -32600, 32 | /// Method not found 33 | JSONRPC2_METHOD_NOT_FOUND = -32601, 34 | /// Invalid parameters 35 | JSONRPC2_INVALID_PARAMS = -32602, 36 | } 37 | 38 | /// A library error 39 | #[derive(Debug)] 40 | pub enum Error { 41 | /// Json error 42 | Json(serde_json::Error), 43 | /// IO Error 44 | Io(io::Error), 45 | /// Error response 46 | Rpc(RpcError), 47 | /// Response has neither error nor result 48 | NoErrorOrResult, 49 | /// Response to a request did not have the expected nonce 50 | NonceMismatch, 51 | /// Response to a request had a jsonrpc field other than "2.0" 52 | VersionMismatch, 53 | } 54 | 55 | impl From for Error { 56 | fn from(e: serde_json::Error) -> Error { 57 | Error::Json(e) 58 | } 59 | } 60 | 61 | impl From for Error { 62 | fn from(e: io::Error) -> Error { 63 | Error::Io(e) 64 | } 65 | } 66 | 67 | impl From for Error { 68 | fn from(e: RpcError) -> Error { 69 | Error::Rpc(e) 70 | } 71 | } 72 | 73 | impl fmt::Display for Error { 74 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 75 | match *self { 76 | Error::Json(ref e) => write!(f, "JSON decode error: {}", e), 77 | Error::Io(ref e) => write!(f, "IO error response: {}", e), 78 | Error::Rpc(ref r) => write!(f, "RPC error response: {:?}", r), 79 | Error::NoErrorOrResult => write!(f, "Malformed RPC response"), 80 | Error::NonceMismatch => write!(f, "Nonce of response did not match nonce of request"), 81 | Error::VersionMismatch => write!(f, "`jsonrpc` field set to non-\"2.0\""), 82 | } 83 | } 84 | } 85 | 86 | impl error::Error for Error { 87 | fn cause(&self) -> Option<&dyn error::Error> { 88 | match *self { 89 | Error::Json(ref e) => Some(e), 90 | _ => None, 91 | } 92 | } 93 | } 94 | 95 | #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 96 | /// A JSONRPC error object 97 | pub struct RpcError { 98 | /// The integer identifier of the error 99 | pub code: i32, 100 | /// A string describing the error 101 | pub message: String, 102 | /// Additional data specific to the error 103 | pub data: Option, 104 | } 105 | -------------------------------------------------------------------------------- /ui/src/icon.rs: -------------------------------------------------------------------------------- 1 | use iced::{alignment, Font, Length, Text}; 2 | 3 | const ICONS: Font = Font::External { 4 | name: "Icons", 5 | bytes: include_bytes!("../static/icons/bootstrap-icons.ttf"), 6 | }; 7 | 8 | fn icon(unicode: char) -> Text { 9 | Text::new(&unicode.to_string()) 10 | .font(ICONS) 11 | .width(Length::Units(20)) 12 | .horizontal_alignment(alignment::Horizontal::Center) 13 | .size(20) 14 | } 15 | 16 | pub fn vault_icon() -> Text { 17 | icon('\u{F65A}') 18 | } 19 | 20 | pub fn bitcoin_icon() -> Text { 21 | icon('\u{F635}') 22 | } 23 | 24 | pub fn history_icon() -> Text { 25 | icon('\u{F292}') 26 | } 27 | 28 | pub fn home_icon() -> Text { 29 | icon('\u{F3FC}') 30 | } 31 | 32 | pub fn unlock_icon() -> Text { 33 | icon('\u{F600}') 34 | } 35 | 36 | pub fn warning_octagon_icon() -> Text { 37 | icon('\u{F337}') 38 | } 39 | 40 | pub fn send_icon() -> Text { 41 | icon('\u{F144}') 42 | } 43 | 44 | pub fn connect_device_icon() -> Text { 45 | icon('\u{F348}') 46 | } 47 | 48 | pub fn connected_device_icon() -> Text { 49 | icon('\u{F350}') 50 | } 51 | 52 | pub fn deposit_icon() -> Text { 53 | icon('\u{F123}') 54 | } 55 | 56 | pub fn calendar_icon() -> Text { 57 | icon('\u{F1E8}') 58 | } 59 | 60 | pub fn turnback_icon() -> Text { 61 | icon('\u{F131}') 62 | } 63 | 64 | pub fn vaults_icon() -> Text { 65 | icon('\u{F1C7}') 66 | } 67 | 68 | pub fn settings_icon() -> Text { 69 | icon('\u{F3E5}') 70 | } 71 | 72 | pub fn block_icon() -> Text { 73 | icon('\u{F1C8}') 74 | } 75 | 76 | pub fn square_icon() -> Text { 77 | icon('\u{F584}') 78 | } 79 | 80 | pub fn square_check_icon() -> Text { 81 | icon('\u{F26D}') 82 | } 83 | 84 | pub fn circle_check_icon() -> Text { 85 | icon('\u{F26B}') 86 | } 87 | 88 | pub fn network_icon() -> Text { 89 | icon('\u{F40D}') 90 | } 91 | 92 | pub fn dot_icon() -> Text { 93 | icon('\u{F287}') 94 | } 95 | 96 | pub fn clipboard_icon() -> Text { 97 | icon('\u{F28E}') 98 | } 99 | 100 | pub fn shield_icon() -> Text { 101 | icon('\u{F53F}') 102 | } 103 | 104 | pub fn shield_notif_icon() -> Text { 105 | icon('\u{F530}') 106 | } 107 | 108 | pub fn shield_check_icon() -> Text { 109 | icon('\u{F52F}') 110 | } 111 | 112 | pub fn person_check_icon() -> Text { 113 | icon('\u{F4D6}') 114 | } 115 | 116 | pub fn person_icon() -> Text { 117 | icon('\u{F4DA}') 118 | } 119 | 120 | pub fn tooltip_icon() -> Text { 121 | icon('\u{F431}') 122 | } 123 | 124 | pub fn plus_icon() -> Text { 125 | icon('\u{F4FE}') 126 | } 127 | 128 | pub fn warning_icon() -> Text { 129 | icon('\u{F33B}') 130 | } 131 | 132 | pub fn trash_icon() -> Text { 133 | icon('\u{F5DE}') 134 | } 135 | 136 | pub fn key_icon() -> Text { 137 | icon('\u{F44F}') 138 | } 139 | 140 | pub fn cross_icon() -> Text { 141 | icon('\u{F62A}') 142 | } 143 | 144 | pub fn pencil_icon() -> Text { 145 | icon('\u{F4CB}') 146 | } 147 | 148 | #[allow(dead_code)] 149 | pub fn stakeholder_icon() -> Text { 150 | icon('\u{F4AE}') 151 | } 152 | 153 | #[allow(dead_code)] 154 | pub fn manager_icon() -> Text { 155 | icon('\u{F4B4}') 156 | } 157 | 158 | pub fn done_icon() -> Text { 159 | icon('\u{F26B}') 160 | } 161 | 162 | pub fn todo_icon() -> Text { 163 | icon('\u{F28A}') 164 | } 165 | -------------------------------------------------------------------------------- /src/installer/step/common.rs: -------------------------------------------------------------------------------- 1 | use revault_ui::component::form; 2 | 3 | use crate::installer::{message, view}; 4 | 5 | use iced::{button::State as Button, text_input, Element}; 6 | 7 | use revaultd::revault_tx::miniscript::DescriptorPublicKey; 8 | use std::str::FromStr; 9 | 10 | #[derive(Clone)] 11 | pub struct ParticipantXpub { 12 | pub xpub: form::Value, 13 | 14 | xpub_input: text_input::State, 15 | delete_button: Button, 16 | } 17 | 18 | impl ParticipantXpub { 19 | pub fn new() -> Self { 20 | Self { 21 | xpub: form::Value::default(), 22 | xpub_input: text_input::State::new(), 23 | delete_button: Button::new(), 24 | } 25 | } 26 | 27 | pub fn update(&mut self, msg: message::ParticipantXpub) { 28 | if let message::ParticipantXpub::XpubEdited(xpub) = msg { 29 | self.xpub.value = xpub; 30 | self.xpub.valid = true; 31 | } 32 | } 33 | 34 | pub fn check_validity(&mut self, network: &bitcoin::Network) { 35 | if let Ok(DescriptorPublicKey::XPub(xpub)) = DescriptorPublicKey::from_str(&self.xpub.value) 36 | { 37 | if *network == bitcoin::Network::Bitcoin { 38 | self.xpub.valid = xpub.xkey.network == bitcoin::Network::Bitcoin; 39 | } else { 40 | self.xpub.valid = xpub.xkey.network == bitcoin::Network::Testnet; 41 | } 42 | } else { 43 | self.xpub.valid = false; 44 | } 45 | } 46 | 47 | pub fn view(&mut self) -> Element { 48 | view::participant_xpub(&self.xpub, &mut self.xpub_input, &mut self.delete_button) 49 | } 50 | } 51 | 52 | #[derive(Clone)] 53 | pub struct RequiredXpub { 54 | pub xpub: form::Value, 55 | 56 | xpub_input: text_input::State, 57 | } 58 | 59 | impl RequiredXpub { 60 | pub fn new() -> Self { 61 | Self { 62 | xpub: form::Value::default(), 63 | xpub_input: text_input::State::new(), 64 | } 65 | } 66 | 67 | pub fn update(&mut self, msg: String) { 68 | self.xpub.value = msg; 69 | self.xpub.valid = true; 70 | } 71 | 72 | pub fn check_validity(&mut self, network: &bitcoin::Network) { 73 | if let Ok(DescriptorPublicKey::XPub(xpub)) = DescriptorPublicKey::from_str(&self.xpub.value) 74 | { 75 | if *network == bitcoin::Network::Bitcoin { 76 | self.xpub.valid = xpub.xkey.network == bitcoin::Network::Bitcoin; 77 | } else { 78 | self.xpub.valid = xpub.xkey.network == bitcoin::Network::Testnet; 79 | } 80 | } else { 81 | self.xpub.valid = false; 82 | } 83 | } 84 | 85 | pub fn view(&mut self) -> Element { 86 | view::required_xpub(&self.xpub, &mut self.xpub_input) 87 | } 88 | } 89 | 90 | pub struct CosignerKey { 91 | pub key: form::Value, 92 | 93 | key_input: text_input::State, 94 | } 95 | 96 | impl CosignerKey { 97 | pub fn new() -> Self { 98 | Self { 99 | key: form::Value::default(), 100 | key_input: text_input::State::new(), 101 | } 102 | } 103 | 104 | pub fn update(&mut self, key: String) { 105 | self.key.value = key; 106 | self.key.valid = true; 107 | } 108 | 109 | pub fn view(&mut self) -> Element { 110 | view::cosigner_key(&self.key, &mut self.key_input) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /contrib/tools/dummysigner/README.md: -------------------------------------------------------------------------------- 1 | 2 | # `dummysigner` 3 | 4 | A simple signer to simulate Hardware wallet usage with 5 | revault-gui. 6 | 7 | Do not use with real funds. 8 | 9 | ## Usage 10 | 11 | The dummysigner can sign with multiple extended private key following 12 | the [bip32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) format. 13 | 14 | ``` 15 | cargo run -- ... 16 | ``` 17 | 18 | The dummysigner handles revault descriptors 19 | 20 | ``` 21 | cargo run -- --conf 22 | ``` 23 | 24 | You can find an example of the configuration file 25 | [here](examples/examples_cfg.toml). 26 | 27 | ## Communication 28 | 29 | ### Transport 30 | 31 | `dummysigner` use a tcp server listening to `0.0.0.0:8080` in order to receive and respond to signature 32 | requests. 33 | 34 | Messages are json objects framed by the [tokio_util length delimited 35 | codec](https://docs.rs/tokio-util/0.6.7/tokio_util/codec/length_delimited/index.html). 36 | 37 | ### Request refused 38 | 39 | If the signature request was refused the response looks like: 40 | 41 | ```json 42 | {"request_status": "refused"} 43 | ``` 44 | 45 | ### Sign spend transaction 46 | 47 | #### request: 48 | 49 | ```json 50 | { 51 | "spend_tx": "" 52 | } 53 | ``` 54 | 55 | #### response: 56 | 57 | ```json 58 | { 59 | "spend_tx": "" 60 | } 61 | ``` 62 | 63 | ### Sign unvault transaction 64 | 65 | #### request: 66 | 67 | ```json 68 | { 69 | "unvault_tx": "" 70 | } 71 | ``` 72 | 73 | #### response: 74 | 75 | ```json 76 | { 77 | "unvault_tx": "" 78 | } 79 | ``` 80 | 81 | ### Sign revocation transactions 82 | 83 | #### request: 84 | 85 | ```json 86 | { 87 | "cancel_txs": [""], 88 | "emergency_tx": "", 89 | "emergency_unvault_tx": "" 90 | } 91 | ``` 92 | 93 | #### response: 94 | 95 | ```json 96 | { 97 | "cancel_tx": [""], 98 | "emergency_tx": "", 99 | "emergency_unvault_tx": "" 100 | } 101 | ``` 102 | 103 | ### Secure deposits in batch 104 | 105 | This method requires the descriptors and the emergency address. 106 | 107 | #### request: 108 | 109 | ```json 110 | { 111 | "deposits": [ 112 | { 113 | "outpoint": ":", 114 | "amount": "", 115 | "derivation_index": "" 116 | }, 117 | ... 118 | ] 119 | } 120 | ``` 121 | 122 | #### response: 123 | 124 | ```json 125 | [ 126 | { 127 | "cancel_txs": [""], 128 | "emergency_tx": "", 129 | "emergency_unvault_tx": "" 130 | }, 131 | ... 132 | ] 133 | ``` 134 | 135 | ### Delegate vaults in batch 136 | 137 | This method requires the descriptors and the emergency address. 138 | 139 | #### request: 140 | 141 | ```json 142 | { 143 | "vaults": [ 144 | { 145 | "outpoint": ":", 146 | "amount": "", 147 | "derivation_index": "" 148 | }, 149 | ... 150 | ] 151 | } 152 | ``` 153 | 154 | #### response: 155 | 156 | ```json 157 | [ 158 | { 159 | "unvault_tx": "" 160 | }, 161 | ... 162 | ] 163 | ``` 164 | 165 | ## Example 166 | 167 | ``` 168 | cargo run -- --conf examples/examples_cfg.toml 169 | ``` 170 | 171 | then run the client: 172 | 173 | ``` 174 | cargo run --example stakeholder_batch 175 | ``` 176 | -------------------------------------------------------------------------------- /src/app/state/deposit.rs: -------------------------------------------------------------------------------- 1 | use std::convert::From; 2 | 3 | use iced::{Command, Element}; 4 | 5 | use super::{cmd::get_deposit_address, State}; 6 | 7 | use crate::app::{ 8 | context::Context, error::Error, message::Message, view::DepositView, view::LoadingDashboard, 9 | }; 10 | 11 | /// DepositState handles the deposit process. 12 | /// It gets a deposit address from the revault daemon and 13 | /// give it to its view in order to be rendered. 14 | #[derive(Debug)] 15 | pub enum DepositState { 16 | Loading { 17 | fail: Option, 18 | view: LoadingDashboard, 19 | }, 20 | Loaded { 21 | address: bitcoin::Address, 22 | // Error in case of reload failure. 23 | warning: Option, 24 | 25 | /// The deposit view is rendering the address. 26 | view: DepositView, 27 | }, 28 | } 29 | 30 | impl DepositState { 31 | pub fn new() -> Self { 32 | DepositState::Loading { 33 | view: LoadingDashboard::default(), 34 | fail: None, 35 | } 36 | } 37 | } 38 | 39 | impl State for DepositState { 40 | fn update(&mut self, ctx: &Context, message: Message) -> Command { 41 | match self { 42 | Self::Loading { fail, .. } => { 43 | if let Message::DepositAddress(res) = message { 44 | match res { 45 | Ok(address) => { 46 | let mut view = DepositView::new(); 47 | view.load(&address); 48 | *self = Self::Loaded { 49 | address, 50 | warning: None, 51 | view, 52 | }; 53 | } 54 | Err(e) => *fail = Some(e.into()), 55 | }; 56 | } 57 | } 58 | Self::Loaded { 59 | address, 60 | warning, 61 | view, 62 | } => { 63 | match message { 64 | Message::Reload => return self.load(ctx), 65 | Message::DepositAddress(res) => { 66 | match res { 67 | Ok(addr) => { 68 | // Address is loaded directly in the view in order to cache the created qrcode. 69 | view.load(&address); 70 | *address = addr; 71 | } 72 | Err(e) => *warning = Some(e.into()), 73 | } 74 | } 75 | _ => {} 76 | } 77 | } 78 | }; 79 | Command::none() 80 | } 81 | 82 | fn view(&mut self, ctx: &Context) -> Element { 83 | match self { 84 | Self::Loading { fail, view } => view.view(ctx, fail.as_ref()), 85 | Self::Loaded { 86 | warning, 87 | address, 88 | view, 89 | } => view.view(ctx, warning.as_ref(), address), 90 | } 91 | } 92 | 93 | fn load(&self, ctx: &Context) -> Command { 94 | let revaultd = ctx.revaultd.clone(); 95 | Command::perform(get_deposit_address(revaultd), Message::DepositAddress) 96 | } 97 | } 98 | 99 | impl From for Box { 100 | fn from(s: DepositState) -> Box { 101 | Box::new(s) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /ui/src/component/form.rs: -------------------------------------------------------------------------------- 1 | use iced::{ 2 | text_input::{self, State, TextInput}, 3 | Column, Container, Length, 4 | }; 5 | 6 | use crate::{color, component::text::Text}; 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct Value { 10 | pub value: T, 11 | pub valid: bool, 12 | } 13 | 14 | impl std::default::Default for Value { 15 | fn default() -> Self { 16 | Self { 17 | value: "".to_string(), 18 | valid: true, 19 | } 20 | } 21 | } 22 | 23 | pub struct Form<'a, Message> { 24 | input: TextInput<'a, Message>, 25 | warning: Option<&'a str>, 26 | valid: bool, 27 | } 28 | 29 | impl<'a, Message: 'a> Form<'a, Message> 30 | where 31 | Message: Clone, 32 | { 33 | /// Creates a new [`Form`]. 34 | /// 35 | /// It expects: 36 | /// - some [`iced::text_input::State`] 37 | /// - a placeholder 38 | /// - the current value 39 | /// - a function that produces a message when the [`Form`] changes 40 | pub fn new( 41 | state: &'a mut State, 42 | placeholder: &str, 43 | value: &Value, 44 | on_change: F, 45 | ) -> Self 46 | where 47 | F: 'static + Fn(String) -> Message, 48 | { 49 | Self { 50 | input: TextInput::new(state, placeholder, &value.value, on_change), 51 | warning: None, 52 | valid: value.valid, 53 | } 54 | } 55 | 56 | /// Sets the [`Form`] with a warning message 57 | pub fn warning(mut self, warning: &'a str) -> Self { 58 | self.warning = Some(warning); 59 | self 60 | } 61 | 62 | /// Sets the padding of the [`Form`]. 63 | pub fn padding(mut self, units: u16) -> Self { 64 | self.input = self.input.padding(units); 65 | self 66 | } 67 | 68 | /// Sets the [`Form`] with a text size 69 | pub fn size(mut self, size: u16) -> Self { 70 | self.input = self.input.size(size); 71 | self 72 | } 73 | 74 | pub fn render(self) -> Container<'a, Message> { 75 | if !self.valid { 76 | if let Some(message) = self.warning { 77 | return Container::new( 78 | Column::with_children(vec![ 79 | self.input.style(InvalidFormStyle).into(), 80 | Text::new(message).small().color(color::ALERT).into(), 81 | ]) 82 | .width(Length::Fill) 83 | .spacing(5), 84 | ) 85 | .width(Length::Fill); 86 | } 87 | } 88 | 89 | Container::new(self.input).width(Length::Fill) 90 | } 91 | } 92 | 93 | struct InvalidFormStyle; 94 | impl text_input::StyleSheet for InvalidFormStyle { 95 | fn active(&self) -> text_input::Style { 96 | text_input::Style { 97 | background: iced::Background::Color(color::FOREGROUND), 98 | border_radius: 5.0, 99 | border_width: 1.0, 100 | border_color: color::ALERT, 101 | } 102 | } 103 | 104 | fn focused(&self) -> text_input::Style { 105 | text_input::Style { 106 | border_color: color::ALERT, 107 | ..self.active() 108 | } 109 | } 110 | 111 | fn placeholder_color(&self) -> iced::Color { 112 | iced::Color::from_rgb(0.7, 0.7, 0.7) 113 | } 114 | 115 | fn value_color(&self) -> iced::Color { 116 | iced::Color::from_rgb(0.3, 0.3, 0.3) 117 | } 118 | 119 | fn selection_color(&self) -> iced::Color { 120 | iced::Color::from_rgb(0.8, 0.8, 1.0) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/app/config.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::path::{Path, PathBuf}; 3 | 4 | #[derive(Debug, Clone, Deserialize, Serialize)] 5 | pub struct Config { 6 | /// Path to revaultd configuration file. 7 | pub revaultd_config_path: PathBuf, 8 | /// log level, can be "info", "debug", "trace". 9 | pub log_level: Option, 10 | /// Use iced debug feature if true. 11 | pub debug: Option, 12 | } 13 | 14 | pub const DEFAULT_FILE_NAME: &str = "revault_gui.toml"; 15 | 16 | impl Config { 17 | pub fn new(revaultd_config_path: PathBuf) -> Self { 18 | Self { 19 | revaultd_config_path, 20 | log_level: None, 21 | debug: None, 22 | } 23 | } 24 | 25 | pub fn from_file(path: &Path) -> Result { 26 | let config = std::fs::read(path) 27 | .map_err(|e| match e.kind() { 28 | std::io::ErrorKind::NotFound => ConfigError::NotFound, 29 | _ => ConfigError::ReadingFile(format!("Reading configuration file: {}", e)), 30 | }) 31 | .and_then(|file_content| { 32 | toml::from_slice::(&file_content).map_err(|e| { 33 | ConfigError::ReadingFile(format!("Parsing configuration file: {}", e)) 34 | }) 35 | })?; 36 | Ok(config) 37 | } 38 | 39 | pub fn default_path() -> Result { 40 | let mut datadir = default_datadir().map_err(|_| { 41 | ConfigError::Unexpected("Could not locate the default datadir directory.".to_owned()) 42 | })?; 43 | datadir.push(DEFAULT_FILE_NAME); 44 | Ok(datadir) 45 | } 46 | 47 | pub fn file_name(network: &bitcoin::Network) -> String { 48 | if *network == bitcoin::Network::Bitcoin { 49 | DEFAULT_FILE_NAME.to_string() 50 | } else { 51 | format!("revault_gui_{}.toml", network) 52 | } 53 | } 54 | } 55 | 56 | #[derive(PartialEq, Eq, Debug, Clone)] 57 | pub enum ConfigError { 58 | NotFound, 59 | ReadingFile(String), 60 | Unexpected(String), 61 | } 62 | 63 | impl std::fmt::Display for ConfigError { 64 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 65 | match self { 66 | Self::NotFound => write!(f, "Config file not found"), 67 | Self::ReadingFile(e) => write!(f, "Error while reading file: {}", e), 68 | Self::Unexpected(e) => write!(f, "Unexpected error: {}", e), 69 | } 70 | } 71 | } 72 | 73 | impl std::error::Error for ConfigError {} 74 | 75 | // From github.com/revault/revaultd: 76 | // Get the absolute path to the revault configuration folder. 77 | /// 78 | /// This a "revault" directory in the XDG standard configuration directory for all OSes but 79 | /// Linux-based ones, for which it's `~/.revault`. 80 | /// Rationale: we want to have the database, RPC socket, etc.. in the same folder as the 81 | /// configuration file but for Linux the XDG specify a data directory (`~/.local/share/`) different 82 | /// from the configuration one (`~/.config/`). 83 | pub fn default_datadir() -> Result { 84 | #[cfg(target_os = "linux")] 85 | let configs_dir = dirs::home_dir(); 86 | 87 | #[cfg(not(target_os = "linux"))] 88 | let configs_dir = dirs::config_dir(); 89 | 90 | if let Some(mut path) = configs_dir { 91 | #[cfg(target_os = "linux")] 92 | path.push(".revault"); 93 | 94 | #[cfg(not(target_os = "linux"))] 95 | path.push("Revault"); 96 | 97 | return Ok(path); 98 | } 99 | 100 | Err(()) 101 | } 102 | -------------------------------------------------------------------------------- /src/app/view/warning.rs: -------------------------------------------------------------------------------- 1 | use std::convert::From; 2 | 3 | use iced::{Column, Container, Length}; 4 | 5 | use revault_ui::component::notification::warning; 6 | use revaultd::commands::ErrorCode; 7 | 8 | use crate::{ 9 | app::error::Error, 10 | daemon::{client::error::RpcErrorCode, RevaultDError}, 11 | }; 12 | 13 | /// Simple warning message displayed to non technical user. 14 | pub struct WarningMessage(String); 15 | 16 | impl From<&Error> for WarningMessage { 17 | fn from(error: &Error) -> WarningMessage { 18 | match error { 19 | Error::Hardware(e) => match e { 20 | revault_hwi::HWIError::DeviceDidNotSign => { 21 | WarningMessage("Device did not sign with user key".to_string()) 22 | } 23 | _ => WarningMessage(e.to_string()), 24 | }, 25 | Error::Config(e) => WarningMessage(e.to_owned()), 26 | // TODO: change when ConfigError is enum again. 27 | // Error::ConfigError(e) => match e { 28 | // ConfigError::NotFound => WarningMessage("Configuration file not fund".to_string()), 29 | // ConfigError::ReadingFile(_) => { 30 | // WarningMessage("Failed to read configuration file".to_string()) 31 | // } 32 | // ConfigError::Unexpected(_) => WarningMessage("Unknown error".to_string()), 33 | // }, 34 | Error::Daemon(e) => match e { 35 | RevaultDError::Rpc(code, _) => { 36 | if *code == ErrorCode::COORDINATOR_SIG_STORE_ERROR as i32 { 37 | WarningMessage("Coordinator could not store the signatures".to_string()) 38 | } else if *code == ErrorCode::COORDINATOR_SPEND_STORE_ERROR as i32 { 39 | WarningMessage( 40 | "Coordinator could not store the spend transaction".to_string(), 41 | ) 42 | } else if *code == ErrorCode::TRANSPORT_ERROR as i32 { 43 | WarningMessage("Failed to communicate with remote server".to_string()) 44 | } else if *code == ErrorCode::COSIGNER_INSANE_ERROR as i32 { 45 | WarningMessage("The cosigner has an anormal behaviour, stop all operations and report to your security team".to_string()) 46 | } else if *code == ErrorCode::COSIGNER_ALREADY_SIGN_ERROR as i32 { 47 | WarningMessage("The cosigner already signed the transaction".to_string()) 48 | } else if *code == RpcErrorCode::JSONRPC2_INVALID_PARAMS as i32 { 49 | WarningMessage("Some fields are invalid".to_string()) 50 | } else { 51 | WarningMessage("Internal error".to_string()) 52 | } 53 | } 54 | RevaultDError::Unexpected(_) => WarningMessage("Unknown error".to_string()), 55 | RevaultDError::Start(_) => { 56 | WarningMessage("Revault daemon failed to start".to_string()) 57 | } 58 | RevaultDError::NoAnswer | RevaultDError::Transport(..) => { 59 | WarningMessage("Communication with Revault daemon failed".to_string()) 60 | } 61 | }, 62 | Error::Unexpected(_) => WarningMessage("Unknown error".to_string()), 63 | } 64 | } 65 | } 66 | 67 | impl std::fmt::Display for WarningMessage { 68 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 69 | write!(f, "{}", self.0) 70 | } 71 | } 72 | 73 | pub fn warn<'a, T: 'a>(error: Option<&Error>) -> Container<'a, T> { 74 | if let Some(w) = error { 75 | let message: WarningMessage = w.into(); 76 | warning(&message.to_string(), &w.to_string()).width(Length::Fill) 77 | } else { 78 | Container::new(Column::new()).width(Length::Fill) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /contrib/tools/dummysigner/examples/stakeholder_client.rs: -------------------------------------------------------------------------------- 1 | use futures::prelude::*; 2 | use serde_json::json; 3 | use tokio::net::TcpStream; 4 | use tokio::time::{sleep, Duration}; 5 | use tokio_serde::formats::*; 6 | use tokio_serde::SymmetricallyFramed; 7 | use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec}; 8 | 9 | /// run dummysigner: 10 | /// cargo run -- xprv9zFeRZgUZaUZBEUq1vPFLpUavHPK5YZ6N2qeqCYe7GLxGVY9SRHuN5Uwd5YN56tMUKe2qPhmvP8fC1GBEAFRAwbJQi86swWvvGM5tXBpJt6 11 | 12 | #[tokio::main] 13 | pub async fn main() { 14 | // Bind a server socket 15 | let mut socket = TcpStream::connect("0.0.0.0:8080").await.unwrap(); 16 | 17 | let (reader, writer) = socket.split(); 18 | 19 | let mut sender = SymmetricallyFramed::new( 20 | FramedWrite::new(writer, LengthDelimitedCodec::new()), 21 | SymmetricalJson::default(), 22 | ); 23 | 24 | let mut receiver = SymmetricallyFramed::new( 25 | FramedRead::new(reader, LengthDelimitedCodec::new()), 26 | SymmetricalJson::::default(), 27 | ); 28 | 29 | sender 30 | .send(json!({ 31 | "cancel_tx": "cHNidP8BAF4CAAAAATdzv51EXeeNc1fv6E852OhRxc67KNaWd+BrA3qN1a/1AAAAAAD9////ARRLJgcAAAAAIgAgdfJpF3TIFneDGEawKCIA4oiyxZcQtY90MYPUklUH28UAAAAAAAEBK7iGJgcAAAAAIgAgSOjPZes2prPdrcgiv+IG1sjXyTCc4KDr9+C9F+xk6LwBAwSBAAAAAQVhIQICkzqxA36tCqSnhYxtSdZwXh+zvF9msAkYr3ufAOzVJqxRh2R2qRRyqV8ir5obrrhS+alScvjCHZjyZIisa3apFLbJrbicjJNybIPiobXZR4nXe5VhiKxsk1KHZ1iyaCIGAgKTOrEDfq0KpKeFjG1J1nBeH7O8X2awCRive58A7NUmCCUdYAkAAAAAIgYCWC3tv0T0ZWTl2M2wZ1NtYOvjTNHRgBz/Ubv516wom0MI1n1/6QAAAAAiBgNHBN7LVbWqiP/R710GNmJIwTFOGWVRE2/xTquLukpJDghyqV8iAAAAAAAiAgJYLe2/RPRlZOXYzbBnU21g6+NM0dGAHP9Ru/nXrCibQwjWfX/pAAAAACICA0cE3stVtaqI/9HvXQY2YkjBMU4ZZVETb/FOq4u6SkkOCHKpXyIAAAAAAA==", 32 | "emergency_tx": "cHNidP8BAF4CAAAAAUeuD/NEqc88sk3DoBrKoVKjXbN2xW8Jr/4GO5q87JqJAQAAAAD9////ATheJgcAAAAAIgAgy7Co1PHzwoce0hHQR5RHMS72lSZudTF3bYrNgqLbkDYAAAAAAAEBKwAOJwcAAAAAIgAgdfJpF3TIFneDGEawKCIA4oiyxZcQtY90MYPUklUH28UBAwSBAAAAAQVHUiECWC3tv0T0ZWTl2M2wZ1NtYOvjTNHRgBz/Ubv516wom0MhA0cE3stVtaqI/9HvXQY2YkjBMU4ZZVETb/FOq4u6SkkOUq4iBgJYLe2/RPRlZOXYzbBnU21g6+NM0dGAHP9Ru/nXrCibQwjWfX/pAAAAACIGA0cE3stVtaqI/9HvXQY2YkjBMU4ZZVETb/FOq4u6SkkOCHKpXyIAAAAAAAA=", 33 | "emergency_unvault_tx": "cHNidP8BAF4CAAAAATdzv51EXeeNc1fv6E852OhRxc67KNaWd+BrA3qN1a/1AAAAAAD9////AWa7JQcAAAAAIgAgy7Co1PHzwoce0hHQR5RHMS72lSZudTF3bYrNgqLbkDYAAAAAAAEBK7iGJgcAAAAAIgAgSOjPZes2prPdrcgiv+IG1sjXyTCc4KDr9+C9F+xk6LwBAwSBAAAAAQVhIQICkzqxA36tCqSnhYxtSdZwXh+zvF9msAkYr3ufAOzVJqxRh2R2qRRyqV8ir5obrrhS+alScvjCHZjyZIisa3apFLbJrbicjJNybIPiobXZR4nXe5VhiKxsk1KHZ1iyaCIGAgKTOrEDfq0KpKeFjG1J1nBeH7O8X2awCRive58A7NUmCCUdYAkAAAAAIgYCWC3tv0T0ZWTl2M2wZ1NtYOvjTNHRgBz/Ubv516wom0MI1n1/6QAAAAAiBgNHBN7LVbWqiP/R710GNmJIwTFOGWVRE2/xTquLukpJDghyqV8iAAAAAAAA" 34 | })) 35 | .await 36 | .unwrap(); 37 | 38 | if let Some(msg) = receiver.try_next().await.unwrap() { 39 | println!("GOT: {:?}", msg); 40 | } 41 | 42 | sleep(Duration::from_secs(2)).await; 43 | 44 | // Send the value 45 | sender 46 | .send(json!({ 47 | "unvault_tx": "cHNidP8BAIkCAAAAAUeuD/NEqc88sk3DoBrKoVKjXbN2xW8Jr/4GO5q87JqJAQAAAAD9////AriGJgcAAAAAIgAgSOjPZes2prPdrcgiv+IG1sjXyTCc4KDr9+C9F+xk6LwwdQAAAAAAACIAIAjkMa8elv7dHUmYpDATWBtmMmpv9yyKFawMunvGQ1AMAAAAAAABASsADicHAAAAACIAIHXyaRd0yBZ3gxhGsCgiAOKIssWXELWPdDGD1JJVB9vFAQMEAQAAAAEFR1IhAlgt7b9E9GVk5djNsGdTbWDr40zR0YAc/1G7+desKJtDIQNHBN7LVbWqiP/R710GNmJIwTFOGWVRE2/xTquLukpJDlKuIgYCWC3tv0T0ZWTl2M2wZ1NtYOvjTNHRgBz/Ubv516wom0MI1n1/6QAAAAAiBgNHBN7LVbWqiP/R710GNmJIwTFOGWVRE2/xTquLukpJDghyqV8iAAAAAAAiAgICkzqxA36tCqSnhYxtSdZwXh+zvF9msAkYr3ufAOzVJgglHWAJAAAAACICAlgt7b9E9GVk5djNsGdTbWDr40zR0YAc/1G7+desKJtDCNZ9f+kAAAAAIgIDRwTey1W1qoj/0e9dBjZiSMExThllURNv8U6ri7pKSQ4IcqlfIgAAAAAAIgICUHL04HZXilyJ1B118e1Smr+S8c1qtja46Le7DzMCaUMI+93szQAAAAAA" 48 | })) 49 | .await 50 | .unwrap(); 51 | 52 | if let Some(msg) = receiver.try_next().await.unwrap() { 53 | println!("GOT: {:?}", msg); 54 | } 55 | 56 | sleep(Duration::from_secs(2)).await; 57 | } 58 | -------------------------------------------------------------------------------- /src/daemon/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | pub mod embedded; 3 | pub mod model; 4 | 5 | use std::collections::BTreeMap; 6 | use std::fmt::Debug; 7 | use std::io::ErrorKind; 8 | 9 | use bitcoin::{util::psbt::PartiallySignedTransaction as Psbt, OutPoint, Txid}; 10 | use revaultd::config::Config; 11 | 12 | use model::*; 13 | 14 | #[derive(Debug, Clone)] 15 | pub enum RevaultDError { 16 | /// Something was wrong with the request. 17 | Rpc(i32, String), 18 | /// Something was wrong with the communication. 19 | Transport(Option, String), 20 | /// Something unexpected happened. 21 | Unexpected(String), 22 | /// No response. 23 | NoAnswer, 24 | // Error at start up. 25 | Start(String), 26 | } 27 | 28 | impl std::fmt::Display for RevaultDError { 29 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 30 | match self { 31 | Self::Rpc(code, e) => write!(f, "Revaultd error rpc call: [{:?}] {}", code, e), 32 | Self::NoAnswer => write!(f, "Revaultd returned no answer"), 33 | Self::Transport(kind, e) => write!(f, "Revaultd transport error: [{:?}] {}", kind, e), 34 | Self::Unexpected(e) => write!(f, "Revaultd unexpected error: {}", e), 35 | Self::Start(e) => write!(f, "Revaultd did not start: {}", e), 36 | } 37 | } 38 | } 39 | 40 | pub trait Daemon: Debug { 41 | fn is_external(&self) -> bool; 42 | 43 | fn load_config(&mut self, _cfg: Config) -> Result<(), RevaultDError> { 44 | return Ok(()); 45 | } 46 | 47 | fn stop(&mut self) -> Result<(), RevaultDError>; 48 | 49 | fn get_deposit_address(&self) -> Result; 50 | 51 | fn get_info(&self) -> Result; 52 | 53 | fn list_vaults( 54 | &self, 55 | statuses: Option<&[VaultStatus]>, 56 | outpoints: Option<&[OutPoint]>, 57 | ) -> Result, RevaultDError>; 58 | 59 | fn list_onchain_transactions( 60 | &self, 61 | outpoints: &[OutPoint], 62 | ) -> Result, RevaultDError>; 63 | 64 | fn list_presigned_transactions( 65 | &self, 66 | outpoints: &[OutPoint], 67 | ) -> Result, RevaultDError>; 68 | 69 | fn get_revocation_txs( 70 | &self, 71 | outpoint: &OutPoint, 72 | ) -> Result; 73 | 74 | fn set_revocation_txs( 75 | &self, 76 | outpoint: &OutPoint, 77 | emergency_tx: &Psbt, 78 | emergency_unvault_tx: &Psbt, 79 | cancel_tx: &[Psbt; 5], 80 | ) -> Result<(), RevaultDError>; 81 | 82 | fn get_unvault_tx(&self, outpoint: &OutPoint) -> Result; 83 | 84 | fn set_unvault_tx(&self, outpoint: &OutPoint, unvault_tx: &Psbt) -> Result<(), RevaultDError>; 85 | 86 | fn get_spend_tx( 87 | &self, 88 | inputs: &[OutPoint], 89 | outputs: &BTreeMap, 90 | feerate: u64, 91 | ) -> Result; 92 | 93 | fn update_spend_tx(&self, psbt: &Psbt) -> Result<(), RevaultDError>; 94 | 95 | fn list_spend_txs( 96 | &self, 97 | statuses: Option<&[SpendTxStatus]>, 98 | ) -> Result, RevaultDError>; 99 | 100 | fn delete_spend_tx(&self, txid: &Txid) -> Result<(), RevaultDError>; 101 | 102 | fn broadcast_spend_tx(&self, txid: &Txid, priority: bool) -> Result<(), RevaultDError>; 103 | 104 | fn revault(&self, outpoint: &OutPoint) -> Result<(), RevaultDError>; 105 | 106 | fn emergency(&self) -> Result<(), RevaultDError>; 107 | 108 | fn get_server_status(&self) -> Result; 109 | 110 | fn get_history( 111 | &self, 112 | kind: &[HistoryEventKind], 113 | start: u32, 114 | end: u32, 115 | limit: u64, 116 | ) -> Result, RevaultDError>; 117 | } 118 | -------------------------------------------------------------------------------- /src/app/view/vaults.rs: -------------------------------------------------------------------------------- 1 | use iced::{pick_list, Alignment, Column, Container, Element, Length, Row}; 2 | 3 | use revault_ui::component::{text::Text, TransparentPickListStyle}; 4 | 5 | use crate::{ 6 | app::{ 7 | context::Context, 8 | error::Error, 9 | message::{Message, VaultFilterMessage}, 10 | view::layout, 11 | }, 12 | daemon::model::{ 13 | VaultStatus, CURRENT_VAULT_STATUSES, MOVED_VAULT_STATUSES, MOVING_VAULT_STATUSES, 14 | }, 15 | }; 16 | 17 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 18 | enum VaultsFilter { 19 | Current, 20 | Moving, 21 | Moved, 22 | } 23 | 24 | impl VaultsFilter { 25 | pub const ALL: [VaultsFilter; 3] = [ 26 | VaultsFilter::Current, 27 | VaultsFilter::Moving, 28 | VaultsFilter::Moved, 29 | ]; 30 | 31 | pub fn new(statuses: &[VaultStatus]) -> VaultsFilter { 32 | if statuses == MOVING_VAULT_STATUSES { 33 | VaultsFilter::Moving 34 | } else if statuses == MOVED_VAULT_STATUSES { 35 | VaultsFilter::Moved 36 | } else { 37 | VaultsFilter::Current 38 | } 39 | } 40 | 41 | pub fn statuses(&self) -> &'static [VaultStatus] { 42 | match self { 43 | Self::Current => &CURRENT_VAULT_STATUSES, 44 | Self::Moving => &MOVING_VAULT_STATUSES, 45 | Self::Moved => &MOVED_VAULT_STATUSES, 46 | } 47 | } 48 | } 49 | 50 | impl std::fmt::Display for VaultsFilter { 51 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 52 | match self { 53 | Self::Current => write!(f, "Current"), 54 | Self::Moving => write!(f, "Moving"), 55 | Self::Moved => write!(f, "Moved"), 56 | } 57 | } 58 | } 59 | 60 | /// VaultsView renders a list of vaults filtered by the status filter. 61 | /// If the loading field is true, only the status pick_list component is displayed. 62 | #[derive(Debug)] 63 | pub struct VaultsView { 64 | dashboard: layout::Dashboard, 65 | pick_filter: pick_list::State, 66 | } 67 | 68 | impl VaultsView { 69 | pub fn new() -> Self { 70 | VaultsView { 71 | dashboard: layout::Dashboard::default(), 72 | pick_filter: pick_list::State::default(), 73 | } 74 | } 75 | 76 | pub fn view<'a>( 77 | &'a mut self, 78 | ctx: &Context, 79 | warning: Option<&Error>, 80 | vaults: Vec>, 81 | vault_status_filter: &[VaultStatus], 82 | ) -> Element<'a, Message> { 83 | let col = Column::new() 84 | .push( 85 | Row::new() 86 | .push( 87 | Container::new( 88 | Row::new() 89 | .push(Text::new(&format!(" {}", vaults.len())).bold()) 90 | .push(Text::new(" vaults")), 91 | ) 92 | .width(Length::Fill), 93 | ) 94 | .push( 95 | pick_list::PickList::new( 96 | &mut self.pick_filter, 97 | &VaultsFilter::ALL[..], 98 | Some(VaultsFilter::new(vault_status_filter)), 99 | |filter| { 100 | Message::FilterVaults(VaultFilterMessage::Status(filter.statuses())) 101 | }, 102 | ) 103 | .text_size(20) 104 | .padding(10) 105 | .width(Length::Units(200)) 106 | .style(TransparentPickListStyle), 107 | ) 108 | .align_items(Alignment::Center), 109 | ) 110 | .push(Column::with_children(vaults).spacing(5)); 111 | 112 | self.dashboard.view(ctx, warning, col.spacing(20)) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/app/message.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use bitcoin::{util::psbt::PartiallySignedTransaction as Psbt, OutPoint}; 4 | use tokio::sync::Mutex; 5 | 6 | use revault_hwi::{app::revault::RevaultHWI, HWIError}; 7 | use revaultd::config::Config as DaemonConfig; 8 | 9 | use crate::{ 10 | app::{error::Error, menu::Menu}, 11 | daemon::{ 12 | model::{ 13 | HistoryEvent, HistoryEventKind, ServersStatuses, SpendTx, SpendTxStatus, Vault, 14 | VaultPresignedTransactions, VaultStatus, VaultTransactions, 15 | }, 16 | RevaultDError, 17 | }, 18 | revault::Role, 19 | }; 20 | 21 | impl Into for Menu { 22 | fn into(self) -> Message { 23 | Message::Menu(self) 24 | } 25 | } 26 | 27 | #[derive(Debug, Clone)] 28 | pub enum Message { 29 | Reload, 30 | Tick, 31 | Event(iced_native::Event), 32 | Clipboard(String), 33 | ChangeRole(Role), 34 | Vaults(Result, RevaultDError>), 35 | VaultsWithPresignedTxs(Result, RevaultDError>), 36 | VaultsWithUnvaultTx(Result, RevaultDError>), 37 | SelectVault(OutPoint), 38 | SelectHistoryEvent(usize), 39 | DelegateVault(OutPoint), 40 | Sign(SignMessage), 41 | DepositsSecured(Result, Error>), 42 | VaultsDelegated(Result, Error>), 43 | Vault(VaultMessage), 44 | FilterVaults(VaultFilterMessage), 45 | FilterTxs(&'static [SpendTxStatus]), 46 | BlockHeight(Result), 47 | ServerStatus(Result), 48 | HistoryEvents(Result, RevaultDError>), 49 | HistoryEvent(HistoryEventMessage), 50 | FilterHistoryEvents(Option), 51 | Menu(Menu), 52 | Next, 53 | Previous, 54 | DepositAddress(Result), 55 | Recipient(usize, RecipientMessage), 56 | Input(usize, InputMessage), 57 | AddRecipient, 58 | SpendTransaction(Result<(SpendTx, u64), RevaultDError>), 59 | SpendTransactions(Result, RevaultDError>), 60 | SpendTx(SpendTxMessage), 61 | Emergency, 62 | EmergencyBroadcasted(Result<(), RevaultDError>), 63 | Close, 64 | Revault, 65 | Revaulted(Result<(), RevaultDError>), 66 | Settings(usize, SettingsMessage), 67 | AddWatchtower, 68 | LoadDaemonConfig(DaemonConfig), 69 | DaemonConfigLoaded(Result<(), Error>), 70 | } 71 | 72 | #[derive(Debug, Clone)] 73 | pub enum SettingsMessage { 74 | Remove, 75 | Edit, 76 | FieldEdited(&'static str, String), 77 | CancelEdit, 78 | ConfirmEdit, 79 | } 80 | 81 | #[derive(Debug, Clone)] 82 | pub enum SpendTxMessage { 83 | FeerateEdited(String), 84 | PsbtEdited(String), 85 | Import, 86 | Generate, 87 | /// Select the SpendTxMessage with the given psbt. 88 | Select(Psbt), 89 | Sign(SignMessage), 90 | Signed(Result<(), RevaultDError>), 91 | Inputs(Result, RevaultDError>), 92 | SpendTransactions(Result, RevaultDError>), 93 | SelectDelete, 94 | UnselectDelete, 95 | Delete, 96 | Deleted(Result<(), RevaultDError>), 97 | Broadcast, 98 | Broadcasted(Result<(), RevaultDError>), 99 | Update, 100 | Updated(Result<(), RevaultDError>), 101 | WithPriority(bool), 102 | } 103 | 104 | #[derive(Debug, Clone)] 105 | pub enum HistoryEventMessage { 106 | OnChainTransactions(Result, RevaultDError>), 107 | } 108 | 109 | #[derive(Debug, Clone)] 110 | pub enum VaultMessage { 111 | ListOnchainTransaction, 112 | OnChainTransactions(Result), 113 | } 114 | 115 | #[derive(Debug, Clone)] 116 | pub enum VaultFilterMessage { 117 | Status(&'static [VaultStatus]), 118 | } 119 | 120 | #[derive(Debug, Clone)] 121 | pub enum SignMessage { 122 | CheckConnection, 123 | Ping(Result<(), HWIError>), 124 | SelectSign, 125 | Connected(Result>>, HWIError>), 126 | PsbtSigned(Result, HWIError>), 127 | } 128 | 129 | #[derive(Debug, Clone)] 130 | pub enum InputMessage { 131 | Select, 132 | } 133 | 134 | #[derive(Debug, Clone)] 135 | pub enum RecipientMessage { 136 | Delete, 137 | AddressEdited(String), 138 | AmountEdited(String), 139 | } 140 | -------------------------------------------------------------------------------- /contrib/tools/dummysigner/src/server.rs: -------------------------------------------------------------------------------- 1 | use std::hash::{Hash, Hasher}; 2 | use std::net::SocketAddr; 3 | use std::sync::Arc; 4 | 5 | use tokio::{net::TcpListener, sync::Mutex}; 6 | use tokio_serde::{formats::SymmetricalJson, SymmetricallyFramed}; 7 | use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec}; 8 | 9 | use iced::futures::TryStreamExt; 10 | use iced_futures::futures; 11 | 12 | use iced::futures::SinkExt; 13 | 14 | #[derive(Debug)] 15 | pub struct Error(String); 16 | 17 | pub fn listen(url: T) -> iced::Subscription { 18 | iced::Subscription::from_recipe(Server { 19 | url: url.to_string(), 20 | }) 21 | } 22 | 23 | pub struct Server { 24 | url: String, 25 | } 26 | 27 | impl iced_native::subscription::Recipe for Server 28 | where 29 | H: Hasher, 30 | { 31 | type Output = ServerMessage; 32 | 33 | fn hash(&self, state: &mut H) { 34 | struct Marker; 35 | std::any::TypeId::of::().hash(state); 36 | self.url.hash(state); 37 | } 38 | 39 | fn stream( 40 | self: Box, 41 | _input: futures::stream::BoxStream<'static, I>, 42 | ) -> futures::stream::BoxStream<'static, Self::Output> { 43 | Box::pin(futures::stream::unfold( 44 | ServerState::Ready(self.url), 45 | move |state| async move { 46 | match state { 47 | ServerState::Ready(url) => match tokio::net::TcpListener::bind(url).await { 48 | Ok(l) => Some((ServerMessage::Started, ServerState::Listening(l))), 49 | Err(_) => Some((ServerMessage::Stopped, ServerState::Stopped)), 50 | }, 51 | ServerState::Listening(listener) => match listener.accept().await { 52 | Ok((socket, addr)) => { 53 | let (read_half, write_half) = socket.into_split(); 54 | let reader = SymmetricallyFramed::new( 55 | FramedRead::new(read_half, LengthDelimitedCodec::new()), 56 | SymmetricalJson::default(), 57 | ); 58 | let writer = SymmetricallyFramed::new( 59 | FramedWrite::new(write_half, LengthDelimitedCodec::new()), 60 | SymmetricalJson::default(), 61 | ); 62 | Some(( 63 | ServerMessage::NewConnection(addr, writer), 64 | ServerState::Connected { listener, reader }, 65 | )) 66 | } 67 | Err(_) => Some((ServerMessage::Stopped, ServerState::Stopped)), 68 | }, 69 | ServerState::Connected { 70 | listener, 71 | mut reader, 72 | } => match reader.try_next().await { 73 | Ok(Some(req)) => Some(( 74 | ServerMessage::Request(req), 75 | ServerState::Connected { listener, reader }, 76 | )), 77 | _ => Some(( 78 | ServerMessage::ConnectionDropped, 79 | ServerState::Listening(listener), 80 | )), 81 | }, 82 | ServerState::Stopped => None, 83 | } 84 | }, 85 | )) 86 | } 87 | } 88 | 89 | pub type Reader = SymmetricallyFramed< 90 | FramedRead, 91 | serde_json::Value, 92 | tokio_serde::formats::Json, 93 | >; 94 | 95 | pub type Writer = SymmetricallyFramed< 96 | FramedWrite, 97 | serde_json::Value, 98 | tokio_serde::formats::Json, 99 | >; 100 | 101 | #[derive(Debug)] 102 | pub enum ServerMessage { 103 | Started, 104 | NewConnection(SocketAddr, Writer), 105 | Request(serde_json::Value), 106 | Responded(Result<(), Error>), 107 | ConnectionDropped, 108 | Stopped, 109 | } 110 | 111 | pub enum ServerState { 112 | Ready(String), 113 | Listening(TcpListener), 114 | Connected { 115 | listener: TcpListener, 116 | reader: Reader, 117 | }, 118 | Stopped, 119 | } 120 | 121 | pub async fn respond(writer: Arc>, value: serde_json::Value) -> Result<(), Error> { 122 | let mut writer = writer.lock().await; 123 | writer.send(value).await.unwrap(); 124 | Ok(()) 125 | } 126 | -------------------------------------------------------------------------------- /src/app/view/layout.rs: -------------------------------------------------------------------------------- 1 | use revault_ui::{ 2 | color, 3 | component::{button, navbar, scroll, text::Text, ContainerBackgroundStyle, TooltipStyle}, 4 | icon, 5 | }; 6 | 7 | use crate::app::{ 8 | context::Context, 9 | error::Error, 10 | message::Message, 11 | view::{sidebar::Sidebar, warning::warn}, 12 | }; 13 | 14 | use iced::{ 15 | container, scrollable, tooltip, Alignment, Column, Container, Element, Length, Row, Tooltip, 16 | }; 17 | 18 | #[derive(Debug, Clone, Default)] 19 | pub struct Dashboard { 20 | sidebar: Sidebar, 21 | scroll: scrollable::State, 22 | } 23 | 24 | impl Dashboard { 25 | pub fn view<'a, T: Into>>( 26 | &'a mut self, 27 | ctx: &Context, 28 | warning: Option<&Error>, 29 | content: T, 30 | ) -> Element<'a, Message> { 31 | Column::new() 32 | .push(navbar()) 33 | .push( 34 | Row::new() 35 | .push( 36 | self.sidebar 37 | .view(ctx) 38 | .width(Length::Shrink) 39 | .height(Length::Fill), 40 | ) 41 | .push( 42 | Column::new().push(warn(warning)).push( 43 | main_section(Container::new(scroll( 44 | &mut self.scroll, 45 | Container::new(content), 46 | ))) 47 | .width(Length::Fill) 48 | .height(Length::Fill), 49 | ), 50 | ), 51 | ) 52 | .width(iced::Length::Fill) 53 | .height(iced::Length::Fill) 54 | .into() 55 | } 56 | } 57 | 58 | pub fn main_section<'a, T: 'a>(menu: Container<'a, T>) -> Container<'a, T> { 59 | Container::new(menu.max_width(1500)) 60 | .padding(20) 61 | .style(MainSectionStyle) 62 | .center_x() 63 | .width(Length::Fill) 64 | .height(Length::Fill) 65 | } 66 | 67 | pub struct MainSectionStyle; 68 | impl container::StyleSheet for MainSectionStyle { 69 | fn style(&self) -> container::Style { 70 | container::Style { 71 | background: color::BACKGROUND.into(), 72 | ..container::Style::default() 73 | } 74 | } 75 | } 76 | 77 | #[derive(Debug, Default)] 78 | pub struct Modal { 79 | scroll: scrollable::State, 80 | close_button: iced::button::State, 81 | } 82 | 83 | impl Modal { 84 | pub fn view<'a, T: Into>>( 85 | &'a mut self, 86 | _ctx: &Context, 87 | warning: Option<&Error>, 88 | content: T, 89 | tooltip: Option<&str>, 90 | close_redirect: Message, 91 | ) -> Element<'a, Message> { 92 | let tt = if let Some(help) = tooltip { 93 | Container::new( 94 | Tooltip::new( 95 | Row::new() 96 | .push(icon::tooltip_icon().size(20)) 97 | .push(Text::new(" Help")), 98 | help, 99 | tooltip::Position::Right, 100 | ) 101 | .gap(5) 102 | .size(20) 103 | .padding(10) 104 | .style(TooltipStyle), 105 | ) 106 | } else { 107 | Container::new(Column::new()) 108 | }; 109 | let col = Column::new() 110 | .push( 111 | Column::new() 112 | .push(warn(warning)) 113 | .push( 114 | Row::new() 115 | .push(tt.width(Length::Fill)) 116 | .push( 117 | Container::new( 118 | button::close_button(&mut self.close_button) 119 | .on_press(close_redirect), 120 | ) 121 | .width(Length::Shrink), 122 | ) 123 | .align_items(Alignment::Center) 124 | .padding(20), 125 | ) 126 | .spacing(20), 127 | ) 128 | .push( 129 | Container::new(Container::new(content).max_width(1500)) 130 | .width(Length::Fill) 131 | .center_x(), 132 | ) 133 | .spacing(50); 134 | 135 | Container::new(scroll(&mut self.scroll, Container::new(col))) 136 | .width(Length::Fill) 137 | .height(Length::Fill) 138 | .style(ContainerBackgroundStyle) 139 | .into() 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/app/context.rs: -------------------------------------------------------------------------------- 1 | use std::fs::OpenOptions; 2 | use std::future::Future; 3 | use std::io::Write; 4 | use std::pin::Pin; 5 | use std::sync::Arc; 6 | 7 | use bitcoin::util::psbt::PartiallySignedTransaction as Psbt; 8 | 9 | use revaultd::config::Config as DaemonConfig; 10 | use revaultd::revault_tx::miniscript::DescriptorPublicKey; 11 | 12 | use revault_hwi::{app::revault::RevaultHWI, HWIError}; 13 | 14 | use crate::{ 15 | app::{config, error::Error, menu::Menu}, 16 | conversion::Converter, 17 | daemon::Daemon, 18 | revault::Role, 19 | }; 20 | 21 | pub type HardwareWallet = 22 | Box, HWIError>> + Send + Sync>; 23 | 24 | /// Context is an object passing general information 25 | /// and service clients through the application components. 26 | pub struct Context { 27 | pub config: ConfigContext, 28 | pub blockheight: i32, 29 | pub revaultd: Arc, 30 | pub converter: Converter, 31 | pub menu: Menu, 32 | pub role: Role, 33 | pub managers_threshold: usize, 34 | pub hardware_wallet: Box Pin + Send + Sync>, 35 | } 36 | 37 | impl Context { 38 | pub fn new( 39 | config: ConfigContext, 40 | revaultd: Arc, 41 | converter: Converter, 42 | role: Role, 43 | menu: Menu, 44 | hardware_wallet: Box Pin + Send + Sync>, 45 | ) -> Self { 46 | Self { 47 | config, 48 | blockheight: 0, 49 | revaultd, 50 | converter, 51 | role, 52 | menu, 53 | managers_threshold: 0, 54 | hardware_wallet, 55 | } 56 | } 57 | 58 | pub fn role_editable(&self) -> bool { 59 | self.config.daemon.stakeholder_config.is_some() 60 | && self.config.daemon.manager_config.is_some() 61 | } 62 | 63 | pub fn network(&self) -> bitcoin::Network { 64 | self.config.daemon.bitcoind_config.network 65 | } 66 | 67 | pub fn stakeholders_xpubs(&self) -> Vec { 68 | self.config.daemon.scripts_config.deposit_descriptor.xpubs() 69 | } 70 | 71 | pub fn managers_xpubs(&self) -> Vec { 72 | // The managers' xpubs are all the xpubs from the Unvault descriptor except the 73 | // Stakehodlers' ones and the Cosigning Servers' ones. 74 | let stk_xpubs = self.stakeholders_xpubs(); 75 | self.config 76 | .daemon 77 | .scripts_config 78 | .unvault_descriptor 79 | .xpubs() 80 | .into_iter() 81 | .filter(|xpub| match xpub { 82 | DescriptorPublicKey::SinglePub(_) => false, 83 | DescriptorPublicKey::XPub(_) => !stk_xpubs.contains(&xpub), 84 | }) 85 | .collect() 86 | } 87 | 88 | pub fn user_signed(&self, psbt: &Psbt) -> bool { 89 | let man_fp = &self 90 | .config 91 | .daemon 92 | .manager_config 93 | .as_ref() 94 | .map(|key| key.xpub.fingerprint()); 95 | let stk_fp = &self 96 | .config 97 | .daemon 98 | .stakeholder_config 99 | .as_ref() 100 | .map(|key| key.xpub.fingerprint()); 101 | if let Some(input) = psbt.inputs.first() { 102 | input.partial_sigs.keys().any(|key| { 103 | input 104 | .bip32_derivation 105 | .get(key) 106 | .map(|(fingerprint, _)| { 107 | Some(*fingerprint) == *man_fp || Some(*fingerprint) == *stk_fp 108 | }) 109 | .unwrap_or(false) 110 | }) 111 | } else { 112 | false 113 | } 114 | } 115 | 116 | pub fn load_daemon_config(&mut self, cfg: DaemonConfig) -> Result<(), Error> { 117 | loop { 118 | if let Some(daemon) = Arc::get_mut(&mut self.revaultd) { 119 | daemon.load_config(cfg)?; 120 | break; 121 | } 122 | } 123 | 124 | let mut daemon_config_file = OpenOptions::new() 125 | .write(true) 126 | .open(&self.config.gui.revaultd_config_path) 127 | .map_err(|e| Error::Config(e.to_string()))?; 128 | 129 | let content = 130 | toml::to_string(&self.config.daemon).map_err(|e| Error::Config(e.to_string()))?; 131 | 132 | daemon_config_file 133 | .write_all(content.as_bytes()) 134 | .map_err(|e| { 135 | log::warn!("failed to write to file: {:?}", e); 136 | Error::Config(e.to_string()) 137 | })?; 138 | 139 | Ok(()) 140 | } 141 | } 142 | 143 | pub struct ConfigContext { 144 | pub daemon: DaemonConfig, 145 | pub gui: config::Config, 146 | } 147 | -------------------------------------------------------------------------------- /ui/src/component/button.rs: -------------------------------------------------------------------------------- 1 | use crate::{color, component::text::Text, icon::clipboard_icon, icon::cross_icon}; 2 | use iced::{button, Alignment, Color, Container, Row, Vector}; 3 | 4 | macro_rules! button { 5 | ($name:ident, $style_name:ident, $bg_color:expr, $text_color:expr) => { 6 | pub fn $name<'a, T: 'a + Clone>( 7 | state: &'a mut button::State, 8 | content: Container<'a, T>, 9 | ) -> button::Button<'a, T> { 10 | button::Button::new(state, content).style($style_name {}) 11 | } 12 | 13 | struct $style_name {} 14 | impl button::StyleSheet for $style_name { 15 | fn active(&self) -> button::Style { 16 | button::Style { 17 | shadow_offset: Vector::default(), 18 | background: $bg_color.into(), 19 | border_radius: 10.0, 20 | border_width: 0.0, 21 | border_color: Color::TRANSPARENT, 22 | text_color: $text_color, 23 | } 24 | } 25 | } 26 | }; 27 | } 28 | 29 | button!(primary, PrimaryStyle, color::PRIMARY, color::FOREGROUND); 30 | button!( 31 | primary_disable, 32 | PrimaryDisableStyle, 33 | color::PRIMARY_LIGHT, 34 | color::FOREGROUND 35 | ); 36 | 37 | button!(cancel, CancelStyle, color::CANCEL, color::FOREGROUND); 38 | 39 | button!(important, ImportantStyle, color::CANCEL, color::FOREGROUND); 40 | 41 | button!(success, SuccessStyle, color::SUCCESS, color::FOREGROUND); 42 | 43 | button!( 44 | transparent, 45 | TransparentStyle, 46 | Color::TRANSPARENT, 47 | Color::BLACK 48 | ); 49 | 50 | pub fn button_content<'a, T: 'a>(icon: Option, text: &str) -> Container<'a, T> { 51 | match icon { 52 | None => Container::new(Text::new(text)) 53 | .width(iced::Length::Fill) 54 | .center_x() 55 | .padding(5), 56 | Some(i) => Container::new( 57 | Row::new() 58 | .push(i) 59 | .push(Text::new(text)) 60 | .spacing(10) 61 | .width(iced::Length::Fill) 62 | .align_items(Alignment::Center), 63 | ) 64 | .width(iced::Length::Fill) 65 | .center_x() 66 | .padding(5), 67 | } 68 | } 69 | 70 | pub fn clipboard<'a, T: 'a + Clone>( 71 | state: &'a mut button::State, 72 | message: T, 73 | ) -> button::Button<'a, T> { 74 | button::Button::new(state, clipboard_icon().size(15)) 75 | .on_press(message) 76 | .style(ClipboardButtonStyle {}) 77 | } 78 | 79 | struct ClipboardButtonStyle {} 80 | impl button::StyleSheet for ClipboardButtonStyle { 81 | fn active(&self) -> button::Style { 82 | button::Style { 83 | shadow_offset: Vector::default(), 84 | background: Color::TRANSPARENT.into(), 85 | border_radius: 10.0, 86 | border_width: 0.0, 87 | border_color: Color::TRANSPARENT, 88 | text_color: Color::BLACK, 89 | } 90 | } 91 | } 92 | 93 | pub fn white_card_button<'a, T: 'a + Clone>( 94 | state: &'a mut button::State, 95 | content: Container<'a, T>, 96 | ) -> button::Button<'a, T> { 97 | button::Button::new(state, content.padding(10)).style(WhiteCardButtonStyle {}) 98 | } 99 | 100 | struct WhiteCardButtonStyle {} 101 | impl button::StyleSheet for WhiteCardButtonStyle { 102 | fn active(&self) -> button::Style { 103 | button::Style { 104 | border_radius: 10.0, 105 | background: color::FOREGROUND.into(), 106 | ..button::Style::default() 107 | } 108 | } 109 | fn hovered(&self) -> button::Style { 110 | button::Style { 111 | border_radius: 10.0, 112 | background: color::FOREGROUND.into(), 113 | border_color: color::SECONDARY, 114 | border_width: 1.0, 115 | ..button::Style::default() 116 | } 117 | } 118 | } 119 | 120 | pub fn close_button<'a, T: 'a + Clone>(state: &'a mut button::State) -> button::Button<'a, T> { 121 | button::Button::new( 122 | state, 123 | Container::new( 124 | Row::new() 125 | .push(cross_icon()) 126 | .push(Text::new("Close")) 127 | .spacing(5) 128 | .width(iced::Length::Fill) 129 | .height(iced::Length::Fill) 130 | .align_items(Alignment::Center), 131 | ) 132 | .padding(10), 133 | ) 134 | .style(CloseButtonStyle {}) 135 | } 136 | 137 | struct CloseButtonStyle {} 138 | impl button::StyleSheet for CloseButtonStyle { 139 | fn active(&self) -> button::Style { 140 | button::Style { 141 | shadow_offset: Vector::default(), 142 | background: color::CANCEL.into(), 143 | border_radius: 10.0, 144 | border_width: 0.0, 145 | border_color: Color::TRANSPARENT, 146 | text_color: Color::WHITE, 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/app/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod context; 3 | pub mod menu; 4 | pub mod message; 5 | pub mod state; 6 | 7 | mod error; 8 | mod view; 9 | 10 | use std::sync::Arc; 11 | use std::time::Duration; 12 | 13 | use iced::{clipboard, time, Command, Element, Subscription}; 14 | use iced_native::{window, Event}; 15 | 16 | pub use config::Config; 17 | pub use message::{Message, SettingsMessage}; 18 | 19 | use menu::Menu; 20 | use state::{ 21 | DepositState, EmergencyState, HistoryState, ManagerCreateSendTransactionState, 22 | ManagerHomeState, ManagerImportSendTransactionState, ManagerSendState, RevaultVaultsState, 23 | SettingsState, StakeholderCreateVaultsState, StakeholderDelegateVaultsState, 24 | StakeholderHomeState, State, VaultsState, 25 | }; 26 | 27 | use crate::{app::context::Context, revault::Role}; 28 | 29 | pub struct App { 30 | should_exit: bool, 31 | state: Box, 32 | context: Context, 33 | } 34 | 35 | pub fn new_state(context: &Context) -> Box { 36 | match (context.role, &context.menu) { 37 | (_, Menu::Deposit) => DepositState::new().into(), 38 | (_, Menu::History) => HistoryState::new().into(), 39 | (_, Menu::Vaults(menu)) => VaultsState::new(menu).into(), 40 | (_, Menu::RevaultVaults) => RevaultVaultsState::default().into(), 41 | (_, Menu::Settings) => SettingsState::new(context).into(), 42 | (Role::Stakeholder, Menu::Home) => StakeholderHomeState::new().into(), 43 | (Role::Stakeholder, Menu::CreateVaults) => StakeholderCreateVaultsState::new().into(), 44 | (Role::Stakeholder, Menu::DelegateFunds) => StakeholderDelegateVaultsState::new().into(), 45 | (Role::Stakeholder, Menu::Emergency) => EmergencyState::new().into(), 46 | (Role::Manager, Menu::Home) => ManagerHomeState::new().into(), 47 | (Role::Manager, Menu::Send) => ManagerSendState::new().into(), 48 | (Role::Manager, Menu::CreateSpend) => ManagerCreateSendTransactionState::new().into(), 49 | (Role::Manager, Menu::ImportSpend) => ManagerImportSendTransactionState::new().into(), 50 | 51 | // If menu is not available for the role, the user is redirected to Home. 52 | (Role::Stakeholder, _) => StakeholderHomeState::new().into(), 53 | (Role::Manager, _) => ManagerHomeState::new().into(), 54 | } 55 | } 56 | 57 | impl App { 58 | pub fn new(context: Context) -> (App, Command) { 59 | let state = new_state(&context); 60 | let cmd = state.load(&context); 61 | ( 62 | Self { 63 | should_exit: false, 64 | state, 65 | context, 66 | }, 67 | cmd, 68 | ) 69 | } 70 | 71 | pub fn subscription(&self) -> Subscription { 72 | Subscription::batch(vec![ 73 | iced_native::subscription::events().map(Message::Event), 74 | time::every(Duration::from_secs(30)).map(|_| Message::Tick), 75 | self.state.subscription(), 76 | ]) 77 | } 78 | 79 | pub fn should_exit(&self) -> bool { 80 | self.should_exit 81 | } 82 | 83 | pub fn stop(&mut self) { 84 | log::info!("Close requested"); 85 | if !self.context.revaultd.is_external() { 86 | log::info!("Stopping internal daemon..."); 87 | if let Some(d) = Arc::get_mut(&mut self.context.revaultd) { 88 | d.stop().expect("Daemon is internal"); 89 | log::info!("Internal daemon stopped"); 90 | self.should_exit = true; 91 | } 92 | } else { 93 | self.should_exit = true; 94 | } 95 | } 96 | 97 | pub fn update(&mut self, message: Message) -> Command { 98 | match message { 99 | Message::Tick => { 100 | let revaultd = self.context.revaultd.clone(); 101 | Command::perform( 102 | async move { revaultd.get_info().map(|res| res.blockheight) }, 103 | Message::BlockHeight, 104 | ) 105 | } 106 | Message::BlockHeight(res) => { 107 | if let Ok(blockheight) = res { 108 | self.context.blockheight = blockheight; 109 | } 110 | Command::none() 111 | } 112 | Message::LoadDaemonConfig(cfg) => { 113 | let res = self.context.load_daemon_config(cfg); 114 | self.update(Message::DaemonConfigLoaded(res)) 115 | } 116 | Message::ChangeRole(role) => { 117 | self.context.role = role; 118 | self.state = new_state(&self.context); 119 | self.state.load(&self.context) 120 | } 121 | Message::Menu(menu) => { 122 | self.context.menu = menu; 123 | self.state = new_state(&self.context); 124 | self.state.load(&self.context) 125 | } 126 | Message::Clipboard(text) => clipboard::write(text), 127 | Message::Event(Event::Window(window::Event::CloseRequested)) => { 128 | self.stop(); 129 | Command::none() 130 | } 131 | _ => self.state.update(&self.context, message), 132 | } 133 | } 134 | 135 | pub fn view(&mut self) -> Element { 136 | self.state.view(&self.context) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/app/view/emergency.rs: -------------------------------------------------------------------------------- 1 | use bitcoin::Amount; 2 | use iced::{Alignment, Column, Container, Element, Length, Row}; 3 | 4 | use revault_ui::{ 5 | color, 6 | component::{button, card, text::Text}, 7 | icon::warning_icon, 8 | }; 9 | 10 | use crate::app::{context::Context, error::Error, menu::Menu, message::Message, view::layout}; 11 | 12 | #[derive(Debug)] 13 | pub struct EmergencyView { 14 | modal: layout::Modal, 15 | emergency_button: iced::button::State, 16 | } 17 | 18 | impl EmergencyView { 19 | pub fn new() -> Self { 20 | EmergencyView { 21 | modal: layout::Modal::default(), 22 | emergency_button: iced::button::State::new(), 23 | } 24 | } 25 | 26 | pub fn view<'a>( 27 | &'a mut self, 28 | ctx: &Context, 29 | vaults_number: usize, 30 | funds_amount: u64, 31 | warning: Option<&Error>, 32 | processing: bool, 33 | ) -> Element<'a, Message> { 34 | let mut emergency_button = button::primary( 35 | &mut self.emergency_button, 36 | button::button_content(None, "Emergency"), 37 | ); 38 | 39 | if !processing { 40 | emergency_button = emergency_button.on_press(Message::Emergency); 41 | } 42 | 43 | let content = if funds_amount != 0 { 44 | Column::new() 45 | .push(warning_icon().color(color::PRIMARY)) 46 | .push( 47 | Column::new() 48 | .push( 49 | Row::new() 50 | .push(Text::new("This action will send")) 51 | .push( 52 | Text::new(&format!( 53 | " {} ", 54 | ctx.converter.converts(Amount::from_sat(funds_amount)) 55 | )) 56 | .bold(), 57 | ) 58 | .push(Text::new(&ctx.converter.unit.to_string())) 59 | .push(Text::new(" from")) 60 | .push(Text::new(&format!(" {} ", vaults_number)).bold()) 61 | .push(Text::new("vaults")), 62 | ) 63 | .push(Text::new("to the Emergency Deep Vault")) 64 | .align_items(Alignment::Center), 65 | ) 66 | .push(emergency_button) 67 | .spacing(30) 68 | .align_items(Alignment::Center) 69 | } else { 70 | Column::new() 71 | .push(warning_icon().color(color::PRIMARY)) 72 | .push(Text::new("No funds to send to the Emergency Deep Vault")) 73 | .spacing(30) 74 | .align_items(Alignment::Center) 75 | }; 76 | 77 | self.modal.view( 78 | ctx, 79 | warning, 80 | card::border_primary(Container::new(content)) 81 | .padding(20) 82 | .center_x() 83 | .width(Length::Fill), 84 | None, 85 | Message::Menu(Menu::Home), 86 | ) 87 | } 88 | } 89 | 90 | #[derive(Debug)] 91 | pub struct EmergencyTriggeredView { 92 | modal: layout::Modal, 93 | } 94 | 95 | impl EmergencyTriggeredView { 96 | pub fn new() -> Self { 97 | EmergencyTriggeredView { 98 | modal: layout::Modal::default(), 99 | } 100 | } 101 | 102 | pub fn view<'a>( 103 | &'a mut self, 104 | ctx: &Context, 105 | vaults_number: usize, 106 | funds_amount: u64, 107 | ) -> Element<'a, Message> { 108 | self.modal.view( 109 | ctx, 110 | None, 111 | card::border_success(Container::new( 112 | Column::new() 113 | .push(warning_icon().color(color::SUCCESS)) 114 | .push( 115 | Column::new() 116 | .push( 117 | Row::new() 118 | .push(Text::new("Sending")) 119 | .push( 120 | Text::new(&format!( 121 | " {} ", 122 | ctx.converter.converts(Amount::from_sat(funds_amount)) 123 | )) 124 | .bold(), 125 | ) 126 | .push(Text::new(&ctx.converter.unit.to_string())) 127 | .push(Text::new(" from")) 128 | .push(Text::new(&format!(" {} ", vaults_number)).bold()) 129 | .push(Text::new("vaults")), 130 | ) 131 | .push(Text::new("to the Emergency Deep Vault")) 132 | .align_items(Alignment::Center), 133 | ) 134 | .spacing(30) 135 | .align_items(Alignment::Center), 136 | )) 137 | .padding(20) 138 | .center_x() 139 | .width(Length::Fill), 140 | None, 141 | Message::Menu(Menu::Home), 142 | ) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /contrib/tools/dummysigner/src/api.rs: -------------------------------------------------------------------------------- 1 | use revault_tx::bitcoin::{ 2 | blockdata::transaction::OutPoint, 3 | util::{bip32::ChildNumber, psbt::PartiallySignedTransaction}, 4 | Amount, 5 | }; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | #[derive(Debug, Clone, Deserialize)] 9 | #[serde(untagged)] 10 | pub enum Request { 11 | RevocationTransactions(RevocationTransactions), 12 | UnvaultTransaction(UnvaultTransaction), 13 | SpendTransaction(SpendTransaction), 14 | SecureBatch(SecureBatch), 15 | DelegateBatch(DelegateBatch), 16 | } 17 | 18 | #[derive(Debug, Clone, Deserialize)] 19 | pub struct SecureBatch { 20 | pub deposits: Vec, 21 | } 22 | 23 | #[derive(Debug, Clone, Deserialize)] 24 | pub struct DelegateBatch { 25 | pub vaults: Vec, 26 | } 27 | 28 | #[derive(Debug, Clone, Serialize, Deserialize)] 29 | pub struct RevocationTransactions { 30 | #[serde(with = "bitcoin_psbt_array")] 31 | pub cancel_txs: [PartiallySignedTransaction; 5], 32 | 33 | #[serde(with = "bitcoin_psbt")] 34 | pub emergency_tx: PartiallySignedTransaction, 35 | 36 | #[serde(with = "bitcoin_psbt")] 37 | pub emergency_unvault_tx: PartiallySignedTransaction, 38 | } 39 | 40 | #[derive(Debug, Clone, Serialize, Deserialize)] 41 | pub struct UnvaultTransaction { 42 | #[serde(with = "bitcoin_psbt")] 43 | pub unvault_tx: PartiallySignedTransaction, 44 | } 45 | 46 | #[derive(Debug, Clone, Serialize, Deserialize)] 47 | pub struct SpendTransaction { 48 | #[serde(with = "bitcoin_psbt")] 49 | pub spend_tx: PartiallySignedTransaction, 50 | } 51 | 52 | #[derive(Debug, Clone, Deserialize)] 53 | pub struct UTXO { 54 | #[serde(with = "bitcoin_outpoint")] 55 | pub outpoint: OutPoint, 56 | #[serde(with = "bitcoin_amount")] 57 | pub amount: Amount, 58 | #[serde(with = "bitcoin_derivation_index")] 59 | pub derivation_index: ChildNumber, 60 | } 61 | 62 | mod bitcoin_outpoint { 63 | use revault_tx::bitcoin::blockdata::transaction::OutPoint; 64 | use serde::{self, Deserialize, Deserializer}; 65 | use std::str::FromStr; 66 | 67 | pub fn deserialize<'de, D>(deserializer: D) -> Result 68 | where 69 | D: Deserializer<'de>, 70 | { 71 | String::deserialize(deserializer) 72 | .and_then(|s| OutPoint::from_str(&s).map_err(serde::de::Error::custom)) 73 | } 74 | } 75 | 76 | mod bitcoin_amount { 77 | use revault_tx::bitcoin::Amount; 78 | use serde::{self, Deserialize, Deserializer}; 79 | 80 | pub fn deserialize<'de, D>(deserializer: D) -> Result 81 | where 82 | D: Deserializer<'de>, 83 | { 84 | u64::deserialize(deserializer).map(|a| Amount::from_sat(a)) 85 | } 86 | } 87 | 88 | mod bitcoin_derivation_index { 89 | use revault_tx::bitcoin::util::bip32::ChildNumber; 90 | use serde::{self, Deserialize, Deserializer}; 91 | 92 | pub fn deserialize<'de, D>(deserializer: D) -> Result 93 | where 94 | D: Deserializer<'de>, 95 | { 96 | u32::deserialize(deserializer) 97 | .and_then(|i| ChildNumber::from_normal_idx(i).map_err(serde::de::Error::custom)) 98 | } 99 | } 100 | 101 | mod bitcoin_psbt { 102 | use revault_tx::bitcoin::{consensus::encode, util::psbt::PartiallySignedTransaction}; 103 | use serde::{self, Deserialize, Deserializer, Serializer}; 104 | 105 | pub fn deserialize<'de, D>(deserializer: D) -> Result 106 | where 107 | D: Deserializer<'de>, 108 | { 109 | let s = String::deserialize(deserializer)?; 110 | let bytes: Vec = base64::decode(&s).map_err(serde::de::Error::custom)?; 111 | encode::deserialize(&bytes).map_err(serde::de::Error::custom) 112 | } 113 | 114 | pub fn serialize<'se, S>( 115 | psbt: &PartiallySignedTransaction, 116 | serializer: S, 117 | ) -> Result 118 | where 119 | S: Serializer, 120 | { 121 | serializer.serialize_str(&base64::encode(encode::serialize(&psbt))) 122 | } 123 | } 124 | 125 | mod bitcoin_psbt_array { 126 | use revault_tx::bitcoin::{consensus::encode, util::psbt::PartiallySignedTransaction as Psbt}; 127 | use serde::{self, ser::SerializeSeq, Deserialize, Deserializer, Serializer}; 128 | 129 | pub fn deserialize<'de, D>(deserializer: D) -> Result<[Psbt; 5], D::Error> 130 | where 131 | D: Deserializer<'de>, 132 | { 133 | let array: [String; 5] = Deserialize::deserialize(deserializer)?; 134 | let to_psbt = |s: &str| -> Result { 135 | let bytes: Vec = base64::decode(s).map_err(serde::de::Error::custom)?; 136 | encode::deserialize(&bytes).map_err(serde::de::Error::custom) 137 | }; 138 | Ok([ 139 | to_psbt(&array[0])?, 140 | to_psbt(&array[1])?, 141 | to_psbt(&array[2])?, 142 | to_psbt(&array[3])?, 143 | to_psbt(&array[4])?, 144 | ]) 145 | } 146 | 147 | pub fn serialize<'se, S>(psbts: &[Psbt; 5], serializer: S) -> Result 148 | where 149 | S: Serializer, 150 | { 151 | let array: Vec = psbts 152 | .iter() 153 | .map(|psbt| base64::encode(encode::serialize(&psbt))) 154 | .collect(); 155 | let mut seq = serializer.serialize_seq(Some(array.len()))?; 156 | for element in array { 157 | seq.serialize_element(&element)?; 158 | } 159 | seq.end() 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/app/state/emergency.rs: -------------------------------------------------------------------------------- 1 | use std::convert::From; 2 | 3 | use iced::{Command, Element}; 4 | 5 | use super::{cmd::list_vaults, State}; 6 | 7 | use crate::daemon::model::VaultStatus; 8 | 9 | use crate::app::{ 10 | context::Context, 11 | error::Error, 12 | menu::Menu, 13 | message::Message, 14 | state::cmd, 15 | view::{EmergencyTriggeredView, EmergencyView, LoadingModal}, 16 | }; 17 | 18 | #[derive(Debug)] 19 | pub enum EmergencyState { 20 | Loading { 21 | fail: Option, 22 | view: LoadingModal, 23 | }, 24 | Loaded { 25 | view: EmergencyView, 26 | 27 | vaults_number: usize, 28 | funds_amount: u64, 29 | 30 | warning: Option, 31 | 32 | processing: bool, 33 | }, 34 | Triggered { 35 | vaults_number: usize, 36 | funds_amount: u64, 37 | view: EmergencyTriggeredView, 38 | }, 39 | } 40 | 41 | impl EmergencyState { 42 | pub fn new() -> Self { 43 | Self::Loading { 44 | view: LoadingModal::default(), 45 | fail: None, 46 | } 47 | } 48 | } 49 | 50 | impl State for EmergencyState { 51 | fn update(&mut self, ctx: &Context, message: Message) -> Command { 52 | match message { 53 | Message::Vaults(res) => match self { 54 | Self::Loading { fail, .. } => match res { 55 | Ok(vaults) => { 56 | if vaults.iter().any(|vault| { 57 | vault.status == VaultStatus::EmergencyVaulting 58 | || vault.status == VaultStatus::EmergencyVaulted 59 | || vault.status == VaultStatus::UnvaultEmergencyVaulting 60 | || vault.status == VaultStatus::UnvaultEmergencyVaulted 61 | }) { 62 | *self = Self::Triggered { 63 | view: EmergencyTriggeredView::new(), 64 | vaults_number: vaults.len(), 65 | funds_amount: vaults 66 | .into_iter() 67 | .fold(0, |acc, vault| acc + vault.amount.as_sat()), 68 | }; 69 | } else { 70 | *self = Self::Loaded { 71 | view: EmergencyView::new(), 72 | vaults_number: vaults.len(), 73 | funds_amount: vaults 74 | .into_iter() 75 | .fold(0, |acc, vault| acc + vault.amount.as_sat()), 76 | warning: None, 77 | processing: false, 78 | }; 79 | } 80 | } 81 | Err(e) => *fail = Error::from(e).into(), 82 | }, 83 | Self::Loaded { 84 | vaults_number, 85 | funds_amount, 86 | warning, 87 | .. 88 | } => match res { 89 | Ok(vaults) => { 90 | *vaults_number = vaults.len(); 91 | *funds_amount = vaults 92 | .into_iter() 93 | .fold(0, |acc, vault| acc + vault.amount.as_sat()); 94 | *warning = None; 95 | } 96 | Err(e) => *warning = Error::from(e).into(), 97 | }, 98 | _ => {} 99 | }, 100 | Message::Emergency => { 101 | if let Self::Loaded { 102 | processing, 103 | warning, 104 | .. 105 | } = self 106 | { 107 | *processing = true; 108 | *warning = None; 109 | return Command::perform( 110 | cmd::emergency(ctx.revaultd.clone()), 111 | Message::EmergencyBroadcasted, 112 | ); 113 | } 114 | } 115 | Message::EmergencyBroadcasted(res) => { 116 | if let Self::Loaded { 117 | processing, 118 | warning, 119 | vaults_number, 120 | funds_amount, 121 | .. 122 | } = self 123 | { 124 | *processing = false; 125 | if let Err(e) = res { 126 | *warning = Some(e.into()); 127 | } else { 128 | *self = Self::Triggered { 129 | view: EmergencyTriggeredView::new(), 130 | vaults_number: *vaults_number, 131 | funds_amount: *funds_amount, 132 | }; 133 | } 134 | } 135 | } 136 | _ => {} 137 | }; 138 | Command::none() 139 | } 140 | 141 | fn view(&mut self, ctx: &Context) -> Element { 142 | match self { 143 | Self::Loading { fail, view } => view.view(ctx, fail.as_ref(), Menu::Home), 144 | Self::Loaded { 145 | view, 146 | funds_amount, 147 | warning, 148 | processing, 149 | vaults_number, 150 | } => view.view( 151 | ctx, 152 | *vaults_number, 153 | *funds_amount, 154 | warning.as_ref(), 155 | *processing, 156 | ), 157 | Self::Triggered { 158 | view, 159 | funds_amount, 160 | vaults_number, 161 | } => view.view(ctx, *vaults_number, *funds_amount), 162 | } 163 | } 164 | 165 | fn load(&self, ctx: &Context) -> Command { 166 | Command::batch(vec![Command::perform( 167 | list_vaults( 168 | ctx.revaultd.clone(), 169 | Some(&[ 170 | VaultStatus::Secured, 171 | VaultStatus::Active, 172 | VaultStatus::Activating, 173 | VaultStatus::Unvaulting, 174 | VaultStatus::Unvaulted, 175 | VaultStatus::EmergencyVaulting, 176 | VaultStatus::EmergencyVaulted, 177 | VaultStatus::UnvaultEmergencyVaulting, 178 | VaultStatus::UnvaultEmergencyVaulted, 179 | ]), 180 | None, 181 | ), 182 | Message::Vaults, 183 | )]) 184 | } 185 | } 186 | 187 | impl From for Box { 188 | fn from(s: EmergencyState) -> Box { 189 | Box::new(s) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/app/state/revault.rs: -------------------------------------------------------------------------------- 1 | use iced::{Command, Element}; 2 | 3 | use bitcoin::OutPoint; 4 | 5 | use crate::{ 6 | app::{ 7 | context::Context, 8 | error::Error, 9 | menu::Menu, 10 | message::Message, 11 | state::State, 12 | view::{ 13 | LoadingModal, RevaultSelectVaultsView, RevaultSuccessView, RevaultVaultListItemView, 14 | }, 15 | }, 16 | daemon::model::{outpoint, Vault, VaultStatus}, 17 | }; 18 | 19 | #[derive(Debug)] 20 | pub enum RevaultVaultsState { 21 | Loading { 22 | fail: Option, 23 | view: LoadingModal, 24 | }, 25 | SelectVaults { 26 | total: u64, 27 | vaults: Vec, 28 | view: RevaultSelectVaultsView, 29 | processing: bool, 30 | warning: Option, 31 | }, 32 | Success { 33 | vaults: Vec, 34 | view: RevaultSuccessView, 35 | }, 36 | } 37 | 38 | impl Default for RevaultVaultsState { 39 | fn default() -> Self { 40 | RevaultVaultsState::Loading { 41 | fail: None, 42 | view: LoadingModal::default(), 43 | } 44 | } 45 | } 46 | 47 | impl State for RevaultVaultsState { 48 | fn update(&mut self, ctx: &Context, message: Message) -> Command { 49 | match self { 50 | Self::Loading { fail, .. } => { 51 | if let Message::Vaults(res) = message { 52 | match res { 53 | Ok(vaults) => { 54 | *self = Self::SelectVaults { 55 | total: vaults.iter().map(|v| v.amount.as_sat()).sum::(), 56 | vaults: vaults.into_iter().map(RevaultVaultListItem::new).collect(), 57 | view: RevaultSelectVaultsView::default(), 58 | warning: None, 59 | processing: false, 60 | }; 61 | } 62 | Err(e) => *fail = Some(e.into()), 63 | }; 64 | } 65 | } 66 | Self::SelectVaults { 67 | vaults, 68 | processing, 69 | warning, 70 | .. 71 | } => match message { 72 | Message::SelectVault(selected_outpoint) => { 73 | if !*processing { 74 | for vlt in vaults.iter_mut() { 75 | if outpoint(&vlt.vault) == selected_outpoint { 76 | vlt.selected = !vlt.selected 77 | } 78 | } 79 | } 80 | } 81 | Message::Revault => { 82 | *processing = true; 83 | let revaultd = ctx.revaultd.clone(); 84 | let outpoints: Vec = vaults 85 | .iter() 86 | .filter_map(|v| { 87 | if v.selected { 88 | Some(outpoint(&v.vault)) 89 | } else { 90 | None 91 | } 92 | }) 93 | .collect(); 94 | return Command::perform( 95 | async move { 96 | for outpoint in outpoints { 97 | revaultd.revault(&outpoint)?; 98 | } 99 | Ok(()) 100 | }, 101 | Message::Revaulted, 102 | ); 103 | } 104 | Message::Revaulted(res) => match res { 105 | Ok(()) => { 106 | *self = Self::Success { 107 | vaults: vaults 108 | .iter() 109 | .filter_map(|v| { 110 | if v.selected { 111 | Some(v.vault.clone()) 112 | } else { 113 | None 114 | } 115 | }) 116 | .collect(), 117 | view: RevaultSuccessView::default(), 118 | } 119 | } 120 | Err(e) => *warning = Some(e.into()), 121 | }, 122 | _ => {} 123 | }, 124 | _ => {} 125 | }; 126 | Command::none() 127 | } 128 | 129 | fn view(&mut self, ctx: &Context) -> Element { 130 | match self { 131 | Self::Loading { fail, view } => view.view(ctx, fail.as_ref(), Menu::Home), 132 | Self::Success { vaults, view } => view.view(ctx, vaults.len()), 133 | Self::SelectVaults { 134 | view, 135 | vaults, 136 | total, 137 | warning, 138 | processing, 139 | } => view.view( 140 | ctx, 141 | vaults 142 | .iter() 143 | .filter(|v| v.selected) 144 | .fold((0, 0), |(count, total), v| { 145 | (count + 1, total + v.vault.amount.as_sat()) 146 | }), 147 | vaults.iter_mut().map(|vault| vault.view(ctx)).collect(), 148 | *total, 149 | warning.as_ref(), 150 | *processing, 151 | ), 152 | } 153 | } 154 | 155 | fn load(&self, ctx: &Context) -> Command { 156 | let revaultd = ctx.revaultd.clone(); 157 | Command::perform( 158 | async move { 159 | revaultd.list_vaults( 160 | Some(&[VaultStatus::Unvaulting, VaultStatus::Unvaulted]), 161 | None, 162 | ) 163 | }, 164 | Message::Vaults, 165 | ) 166 | } 167 | } 168 | 169 | impl From for Box { 170 | fn from(s: RevaultVaultsState) -> Box { 171 | Box::new(s) 172 | } 173 | } 174 | 175 | #[derive(Debug)] 176 | pub struct RevaultVaultListItem { 177 | vault: Vault, 178 | selected: bool, 179 | view: RevaultVaultListItemView, 180 | } 181 | 182 | impl RevaultVaultListItem { 183 | pub fn new(vault: Vault) -> Self { 184 | Self { 185 | vault, 186 | selected: false, 187 | view: RevaultVaultListItemView::default(), 188 | } 189 | } 190 | 191 | pub fn is_selected(&self) -> bool { 192 | self.selected 193 | } 194 | 195 | pub fn view(&mut self, ctx: &Context) -> Element { 196 | self.view.view(ctx, &self.vault, self.selected) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/app/state/vaults.rs: -------------------------------------------------------------------------------- 1 | use std::convert::From; 2 | 3 | use bitcoin::OutPoint; 4 | use iced::{Command, Element}; 5 | 6 | use super::{ 7 | cmd::list_vaults, 8 | vault::{Vault, VaultListItem}, 9 | State, 10 | }; 11 | 12 | use crate::daemon::{ 13 | model, 14 | model::{ 15 | outpoint, VaultStatus, CURRENT_VAULT_STATUSES, MOVED_VAULT_STATUSES, MOVING_VAULT_STATUSES, 16 | }, 17 | }; 18 | 19 | use crate::app::{ 20 | context::Context, 21 | error::Error, 22 | menu::VaultsMenu, 23 | message::{Message, VaultFilterMessage}, 24 | view::{vault::VaultListItemView, LoadingDashboard, VaultsView}, 25 | }; 26 | 27 | #[derive(Debug)] 28 | pub enum VaultsState { 29 | Loading { 30 | fail: Option, 31 | view: LoadingDashboard, 32 | vault_status_filter: &'static [VaultStatus], 33 | }, 34 | Loaded { 35 | selected_vault: Option, 36 | vault_status_filter: &'static [VaultStatus], 37 | vaults: Vec>, 38 | warning: Option, 39 | 40 | view: VaultsView, 41 | }, 42 | } 43 | 44 | impl VaultsState { 45 | pub fn new(menu: &VaultsMenu) -> Self { 46 | Self::Loading { 47 | view: LoadingDashboard::default(), 48 | fail: None, 49 | vault_status_filter: match menu { 50 | VaultsMenu::Current => &CURRENT_VAULT_STATUSES, 51 | VaultsMenu::Moving => &MOVING_VAULT_STATUSES, 52 | VaultsMenu::Moved => &MOVED_VAULT_STATUSES, 53 | }, 54 | } 55 | } 56 | 57 | pub fn update_vaults(&mut self, vlts: Vec) { 58 | match self { 59 | Self::Loading { 60 | vault_status_filter, 61 | .. 62 | } => { 63 | let vaults = vlts.into_iter().map(VaultListItem::new).collect(); 64 | *self = Self::Loaded { 65 | view: VaultsView::new(), 66 | vault_status_filter, 67 | vaults, 68 | selected_vault: None, 69 | warning: None, 70 | }; 71 | } 72 | Self::Loaded { 73 | vaults, warning, .. 74 | } => { 75 | *vaults = vlts.into_iter().map(VaultListItem::new).collect(); 76 | *warning = None; 77 | } 78 | } 79 | } 80 | 81 | pub fn on_error(&mut self, error: Error) { 82 | match self { 83 | Self::Loading { fail, .. } => { 84 | *fail = Some(error); 85 | } 86 | Self::Loaded { warning, .. } => { 87 | *warning = Some(error); 88 | } 89 | } 90 | } 91 | 92 | pub fn on_vault_select( 93 | &mut self, 94 | ctx: &Context, 95 | selected_outpoint: OutPoint, 96 | ) -> Command { 97 | if let Self::Loaded { 98 | selected_vault, 99 | vaults, 100 | .. 101 | } = self 102 | { 103 | if let Some(selected) = vaults 104 | .iter() 105 | .find(|vlt| outpoint(&vlt.vault) == selected_outpoint) 106 | { 107 | let vault = Vault::new(selected.vault.clone()); 108 | let cmd = vault.load(ctx.revaultd.clone()); 109 | *selected_vault = Some(vault); 110 | return cmd.map(Message::Vault); 111 | }; 112 | }; 113 | Command::none() 114 | } 115 | } 116 | 117 | impl State for VaultsState { 118 | fn update(&mut self, ctx: &Context, message: Message) -> Command { 119 | match message { 120 | Message::Reload => return self.load(ctx), 121 | Message::Vaults(res) => match res { 122 | Ok(vaults) => self.update_vaults(vaults), 123 | Err(e) => self.on_error(Error::from(e)), 124 | }, 125 | Message::SelectVault(outpoint) => return self.on_vault_select(ctx, outpoint), 126 | Message::Close => { 127 | if let Self::Loaded { selected_vault, .. } = self { 128 | if selected_vault.is_some() { 129 | *selected_vault = None; 130 | } 131 | } 132 | } 133 | Message::Vault(msg) => { 134 | if let Self::Loaded { selected_vault, .. } = self { 135 | if let Some(selected) = selected_vault { 136 | return selected.update(ctx, msg).map(Message::Vault); 137 | } 138 | } 139 | } 140 | Message::FilterVaults(VaultFilterMessage::Status(statuses)) => { 141 | if let Self::Loaded { 142 | vault_status_filter, 143 | .. 144 | } = self 145 | { 146 | *vault_status_filter = statuses; 147 | return Command::perform( 148 | list_vaults(ctx.revaultd.clone(), Some(vault_status_filter), None), 149 | Message::Vaults, 150 | ); 151 | } 152 | } 153 | _ => {} 154 | }; 155 | Command::none() 156 | } 157 | 158 | fn view(&mut self, ctx: &Context) -> Element { 159 | match self { 160 | Self::Loading { fail, view, .. } => view.view(ctx, fail.as_ref()), 161 | Self::Loaded { 162 | selected_vault, 163 | vaults, 164 | vault_status_filter, 165 | view, 166 | warning, 167 | .. 168 | } => { 169 | if let Some(v) = selected_vault { 170 | return v.view(ctx); 171 | } 172 | view.view( 173 | ctx, 174 | warning.as_ref(), 175 | vaults.iter_mut().map(|v| v.view(ctx)).collect(), 176 | vault_status_filter, 177 | ) 178 | } 179 | } 180 | } 181 | 182 | fn load(&self, ctx: &Context) -> Command { 183 | match self { 184 | Self::Loading { 185 | vault_status_filter, 186 | .. 187 | } => Command::batch(vec![Command::perform( 188 | list_vaults(ctx.revaultd.clone(), Some(vault_status_filter), None), 189 | Message::Vaults, 190 | )]), 191 | Self::Loaded { 192 | vault_status_filter, 193 | .. 194 | } => Command::batch(vec![Command::perform( 195 | list_vaults(ctx.revaultd.clone(), Some(vault_status_filter), None), 196 | Message::Vaults, 197 | )]), 198 | } 199 | } 200 | } 201 | 202 | impl From for Box { 203 | fn from(s: VaultsState) -> Box { 204 | Box::new(s) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /tests/app_revault.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use serde_json::json; 4 | use std::path::PathBuf; 5 | use std::str::FromStr; 6 | use std::sync::Arc; 7 | 8 | use utils::{fixtures::random_daemon_config, mock::Daemon, no_hardware_wallet, sandbox::Sandbox}; 9 | 10 | use bitcoin::{util::bip32, Address, Amount, OutPoint}; 11 | 12 | use revault_gui::{ 13 | app::{ 14 | config::Config as GUIConfig, 15 | context::{ConfigContext, Context}, 16 | menu::Menu, 17 | message::Message, 18 | state::RevaultVaultsState, 19 | }, 20 | conversion::Converter, 21 | daemon::{ 22 | client::{ListVaultsResponse, RevaultD}, 23 | model::{Vault, VaultStatus}, 24 | }, 25 | revault::Role, 26 | }; 27 | 28 | #[tokio::test] 29 | async fn test_revault_state() { 30 | let daemon = Daemon::new(vec![ 31 | ( 32 | Some(json!({"method": "listvaults", "params": Some(&[[ 33 | VaultStatus::Unvaulting.to_string(), 34 | VaultStatus::Unvaulted.to_string(), 35 | ]])})), 36 | Ok(json!(ListVaultsResponse { 37 | vaults: vec![ 38 | Vault { 39 | address: Address::from_str( 40 | "tb1qkldgvljmjpxrjq2ev5qxe8dvhn0dph9q85pwtfkjeanmwdue2akqj4twxj" 41 | ) 42 | .unwrap(), 43 | amount: Amount::from_sat(500), 44 | derivation_index: bip32::ChildNumber::from_normal_idx(0).unwrap(), 45 | status: VaultStatus::Unvaulting, 46 | txid: bitcoin::Txid::from_str( 47 | "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d" 48 | ) 49 | .unwrap(), 50 | vout: 0, 51 | blockheight: Some(1), 52 | delegated_at: None, 53 | secured_at: Some(1), 54 | funded_at: Some(1), 55 | moved_at: None 56 | }, 57 | Vault { 58 | address: Address::from_str( 59 | "tb1qkldgvljmjpxrjq2ev5qxe8dvhn0dph9q85pwtfkjeanmwdue2akqj4twxj" 60 | ) 61 | .unwrap(), 62 | amount: Amount::from_sat(700), 63 | derivation_index: bip32::ChildNumber::from_normal_idx(0).unwrap(), 64 | status: VaultStatus::Unvaulted, 65 | txid: bitcoin::Txid::from_str( 66 | "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d" 67 | ) 68 | .unwrap(), 69 | vout: 1, 70 | blockheight: Some(1), 71 | delegated_at: None, 72 | secured_at: Some(1), 73 | funded_at: Some(1), 74 | moved_at: None 75 | }, 76 | Vault { 77 | address: Address::from_str( 78 | "tb1qkldgvljmjpxrjq2ev5qxe8dvhn0dph9q85pwtfkjeanmwdue2akqj4twxj" 79 | ) 80 | .unwrap(), 81 | amount: Amount::from_sat(700), 82 | derivation_index: bip32::ChildNumber::from_normal_idx(0).unwrap(), 83 | status: VaultStatus::Unvaulted, 84 | txid: bitcoin::Txid::from_str( 85 | "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d" 86 | ) 87 | .unwrap(), 88 | vout: 2, 89 | blockheight: Some(1), 90 | delegated_at: None, 91 | secured_at: Some(1), 92 | funded_at: Some(1), 93 | moved_at: None 94 | } 95 | ] 96 | })), 97 | ), 98 | ( 99 | Some(json!({"method": "revault", "params": Some(&[ 100 | OutPoint::from_str( 101 | "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d:1", 102 | ) 103 | .unwrap() 104 | ])})), 105 | Ok(json!({})), 106 | ), 107 | ( 108 | Some(json!({"method": "revault", "params": Some(&[ 109 | OutPoint::from_str( 110 | "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d:2", 111 | ) 112 | .unwrap() 113 | ])})), 114 | Ok(json!({})), 115 | ), 116 | ]); 117 | 118 | let sandbox: Sandbox = Sandbox::new(RevaultVaultsState::default()); 119 | 120 | let client = daemon.run(); 121 | let ctx = Context::new( 122 | ConfigContext { 123 | daemon: random_daemon_config(), 124 | gui: GUIConfig::new(PathBuf::from_str("revault_gui.toml").unwrap()), 125 | }, 126 | Arc::new(RevaultD::new(client)), 127 | Converter::new(bitcoin::Network::Bitcoin), 128 | Role::Stakeholder, 129 | Menu::RevaultVaults, 130 | Box::new(|| Box::pin(no_hardware_wallet())), 131 | ); 132 | 133 | let sandbox = sandbox.load(&ctx).await; 134 | assert!(matches!( 135 | sandbox.state(), 136 | RevaultVaultsState::SelectVaults { .. } 137 | )); 138 | 139 | if let RevaultVaultsState::SelectVaults { vaults, .. } = sandbox.state() { 140 | assert_eq!(vaults.len(), 3); 141 | } 142 | 143 | // select vault 2 144 | let sandbox = sandbox 145 | .update( 146 | &ctx, 147 | Message::SelectVault( 148 | OutPoint::from_str( 149 | "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d:1", 150 | ) 151 | .unwrap(), 152 | ), 153 | ) 154 | .await; 155 | 156 | // select vault 3 157 | let sandbox = sandbox 158 | .update( 159 | &ctx, 160 | Message::SelectVault( 161 | OutPoint::from_str( 162 | "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d:2", 163 | ) 164 | .unwrap(), 165 | ), 166 | ) 167 | .await; 168 | 169 | assert!(matches!( 170 | sandbox.state(), 171 | RevaultVaultsState::SelectVaults { .. } 172 | )); 173 | 174 | if let RevaultVaultsState::SelectVaults { vaults, .. } = sandbox.state() { 175 | assert_eq!(vaults.iter().filter(|v| v.is_selected()).count(), 2); 176 | } 177 | 178 | let sandbox = sandbox.update(&ctx, Message::Revault).await; 179 | assert!(matches!( 180 | sandbox.state(), 181 | RevaultVaultsState::Success { .. } 182 | )); 183 | 184 | if let RevaultVaultsState::Success { vaults, .. } = sandbox.state() { 185 | assert_eq!(vaults.len(), 2); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /hwi/src/specter.rs: -------------------------------------------------------------------------------- 1 | use bitcoin::{base64, consensus::encode, util::psbt::PartiallySignedTransaction as Psbt}; 2 | 3 | use serialport::{available_ports, SerialPortType}; 4 | use tokio::io::AsyncBufReadExt; 5 | use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; 6 | pub use tokio::net::TcpStream; 7 | use tokio::net::ToSocketAddrs; 8 | use tokio_serial::SerialPortBuilderExt; 9 | pub use tokio_serial::SerialStream; 10 | 11 | use super::{HWIError, HWI}; 12 | use async_trait::async_trait; 13 | 14 | #[derive(Debug)] 15 | pub struct Specter { 16 | transport: T, 17 | } 18 | 19 | impl Specter { 20 | pub async fn fingerprint(&mut self) -> Result { 21 | self.request("\r\n\r\nfingerprint\r\n").await 22 | } 23 | 24 | pub async fn sign(&mut self, psbt: &Psbt) -> Result { 25 | let mut new_psbt: Psbt = self 26 | .request(&format!( 27 | "\r\n\r\nsign {}\r\n", 28 | base64::encode(&encode::serialize(&psbt)) 29 | )) 30 | .await 31 | .and_then(|resp| base64::decode(&resp).map_err(|e| SpecterError::Device(e.to_string()))) 32 | .and_then(|bytes| { 33 | encode::deserialize(&bytes).map_err(|e| SpecterError::Device(e.to_string())) 34 | })?; 35 | 36 | // Psbt returned by specter wallet has all unnecessary fields removed, 37 | // only global transaction and partial signatures for all inputs remain in it. 38 | // In order to have the full Psbt, the partial_sigs are extracted and appended 39 | // to the original psbt. 40 | let mut psbt = psbt.clone(); 41 | let mut has_signed = false; 42 | for i in 0..new_psbt.inputs.len() { 43 | if !new_psbt.inputs[i].partial_sigs.is_empty() { 44 | has_signed = true; 45 | psbt.inputs[i] 46 | .partial_sigs 47 | .append(&mut new_psbt.inputs[i].partial_sigs) 48 | } 49 | } 50 | 51 | if !has_signed { 52 | return Err(SpecterError::DeviceDidNotSign); 53 | } 54 | 55 | Ok(psbt) 56 | } 57 | 58 | async fn request(&mut self, req: &str) -> Result { 59 | self.transport 60 | .write_all(req.as_bytes()) 61 | .await 62 | .map_err(|e| SpecterError::Device(e.to_string()))?; 63 | 64 | let reader = tokio::io::BufReader::new(&mut self.transport); 65 | let mut lines = reader.lines(); 66 | if let Some(line) = lines 67 | .next_line() 68 | .await 69 | .map_err(|e| SpecterError::Device(e.to_string()))? 70 | { 71 | if line != "ACK" { 72 | return Err(SpecterError::Device( 73 | "Received an incorrect answer".to_string(), 74 | )); 75 | } 76 | } 77 | 78 | if let Some(line) = lines 79 | .next_line() 80 | .await 81 | .map_err(|e| SpecterError::Device(e.to_string()))? 82 | { 83 | return Ok(line); 84 | } 85 | Err(SpecterError::Device("Unexpected".to_string())) 86 | } 87 | } 88 | 89 | pub const SPECTER_SIMULATOR_DEFAULT_ADDRESS: &str = "127.0.0.1:8789"; 90 | 91 | impl Specter { 92 | pub async fn try_connect_simulator( 93 | address: T, 94 | ) -> Result { 95 | let transport = TcpStream::connect(address) 96 | .await 97 | .map_err(|e| SpecterError::Device(e.to_string()))?; 98 | Ok(Specter { transport }) 99 | } 100 | } 101 | #[async_trait] 102 | impl HWI for Specter { 103 | async fn is_connected(&mut self) -> Result<(), HWIError> { 104 | self.fingerprint() 105 | .await 106 | .map_err(|_| HWIError::DeviceDisconnected)?; 107 | Ok(()) 108 | } 109 | async fn sign_tx(&mut self, tx: &Psbt) -> Result { 110 | self.sign(tx).await.map_err(|e| e.into()) 111 | } 112 | } 113 | 114 | const SPECTER_VID: u16 = 61525; 115 | const SPECTER_PID: u16 = 38914; 116 | 117 | impl Specter { 118 | pub fn get_serial_port() -> Result { 119 | match available_ports() { 120 | Ok(ports) => ports 121 | .iter() 122 | .find_map(|p| { 123 | if let SerialPortType::UsbPort(info) = &p.port_type { 124 | if info.vid == SPECTER_VID && info.pid == SPECTER_PID { 125 | Some(p.port_name.clone()) 126 | } else { 127 | None 128 | } 129 | } else { 130 | None 131 | } 132 | }) 133 | .ok_or(SpecterError::DeviceNotFound), 134 | Err(e) => Err(SpecterError::Device(format!( 135 | "Error listing serial ports: {}", 136 | e 137 | ))), 138 | } 139 | } 140 | pub fn try_connect_serial() -> Result { 141 | let tty = Self::get_serial_port()?; 142 | let transport = tokio_serial::new(tty, 9600) 143 | .open_native_async() 144 | .map_err(|e| SpecterError::Device(e.to_string()))?; 145 | Ok(Specter { transport }) 146 | } 147 | } 148 | 149 | #[async_trait] 150 | impl HWI for Specter { 151 | async fn is_connected(&mut self) -> Result<(), HWIError> { 152 | Self::get_serial_port().map_err(|_| HWIError::DeviceDisconnected)?; 153 | Ok(()) 154 | } 155 | async fn sign_tx(&mut self, tx: &Psbt) -> Result { 156 | self.sign(tx).await.map_err(|e| e.into()) 157 | } 158 | } 159 | 160 | #[derive(Debug)] 161 | pub enum SpecterError { 162 | DeviceNotFound, 163 | DeviceDidNotSign, 164 | Device(String), 165 | } 166 | 167 | impl std::fmt::Display for SpecterError { 168 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 169 | match self { 170 | Self::DeviceNotFound => write!(f, "Specter not found"), 171 | Self::DeviceDidNotSign => write!(f, "Specter did not sign the psbt"), 172 | Self::Device(e) => write!(f, "Specter error: {}", e), 173 | } 174 | } 175 | } 176 | 177 | impl From for HWIError { 178 | fn from(e: SpecterError) -> HWIError { 179 | match e { 180 | SpecterError::DeviceNotFound => HWIError::DeviceNotFound, 181 | SpecterError::DeviceDidNotSign => HWIError::DeviceDidNotSign, 182 | SpecterError::Device(e) => HWIError::Device(e), 183 | } 184 | } 185 | } 186 | 187 | #[cfg(feature = "revault")] 188 | mod revault { 189 | use super::{SerialStream, Specter, TcpStream}; 190 | use crate::app::revault::{NoRevaultApp, RevaultHWI}; 191 | 192 | impl From> for Box { 193 | fn from(s: Specter) -> Box { 194 | Box::new(s) 195 | } 196 | } 197 | 198 | impl From> for Box { 199 | fn from(s: Specter) -> Box { 200 | Box::new(s) 201 | } 202 | } 203 | 204 | impl NoRevaultApp for Specter {} 205 | } 206 | -------------------------------------------------------------------------------- /src/app/view/revault.rs: -------------------------------------------------------------------------------- 1 | use iced::{Alignment, Column, Container, Element, Length, Row}; 2 | 3 | use bitcoin::Amount; 4 | use revault_ui::{ 5 | color, 6 | component::{badge, button, text::Text, ContainerForegroundStyle}, 7 | util::Collection, 8 | }; 9 | 10 | use crate::app::{context::Context, error::Error, menu::Menu, message::Message, view::layout}; 11 | 12 | use crate::daemon::model::{outpoint, Vault}; 13 | 14 | #[derive(Debug, Default)] 15 | pub struct RevaultSelectVaultsView { 16 | modal: layout::Modal, 17 | next_button: iced::button::State, 18 | } 19 | 20 | impl RevaultSelectVaultsView { 21 | pub fn view<'a>( 22 | &'a mut self, 23 | ctx: &Context, 24 | selected: (usize, u64), 25 | vaults: Vec>, 26 | total: u64, 27 | warning: Option<&Error>, 28 | processing: bool, 29 | ) -> Element<'a, Message> { 30 | let col = Column::new() 31 | .push( 32 | Column::new() 33 | .push(Text::new("Cancel the movement of funds").bold().size(50)) 34 | .spacing(5), 35 | ) 36 | .push( 37 | Column::new().push( 38 | Row::new() 39 | .push( 40 | Text::new(&ctx.converter.converts(Amount::from_sat(total))) 41 | .bold() 42 | .size(30), 43 | ) 44 | .push(Text::new(&format!(" {} are moving", ctx.converter.unit))) 45 | .align_items(Alignment::Center), 46 | ), 47 | ) 48 | .push( 49 | Column::new() 50 | .push(Text::new("Select vaults to revault").width(Length::Fill)) 51 | .push(Column::with_children(vaults).spacing(5)) 52 | .width(Length::Fill) 53 | .spacing(20), 54 | ); 55 | 56 | Column::new().push(self.modal.view( 57 | ctx, 58 | warning, 59 | col.spacing(30).padding(20).max_width(1000), 60 | Some("By revaulting, you are broadcasting cancel transaction to the network\n, reverting the movement of funds back to the stakeholders wallet."), 61 | Message::Menu(Menu::Home), 62 | )).push_maybe( 63 | if selected.0 > 0 { 64 | Some(Container::new( 65 | Row::new() 66 | .push( 67 | Row::new() 68 | .push( 69 | Text::new( 70 | &ctx.converter 71 | .converts(Amount::from_sat(selected.1)) 72 | ) 73 | .bold(), 74 | ) 75 | .push(Text::new(&format!(" {} (", ctx.converter.unit))) 76 | .push(Text::new(&format!("{}", selected.0)).bold()) 77 | .push(Text::new(" vaults)")) 78 | .width(Length::Fill), 79 | ) 80 | .push( 81 | Container::new( 82 | if processing { 83 | button::primary( 84 | &mut self.next_button, 85 | button::button_content(None, "Revault"), 86 | ) 87 | .width(Length::Units(200)) 88 | } else { 89 | button::primary( 90 | &mut self.next_button, 91 | button::button_content(None, "Revault"), 92 | ) 93 | .on_press(Message::Revault) 94 | .width(Length::Units(200)) 95 | } 96 | ) 97 | .width(Length::Shrink) 98 | ) 99 | .align_items(Alignment::Center) 100 | .max_width(1000), 101 | ) 102 | .padding(30) 103 | .width(Length::Fill) 104 | .center_x() 105 | .style(ContainerForegroundStyle), 106 | ) 107 | } else {None}).into() 108 | } 109 | } 110 | 111 | #[derive(Debug, Default)] 112 | pub struct RevaultSuccessView { 113 | modal: layout::Modal, 114 | } 115 | 116 | impl RevaultSuccessView { 117 | pub fn view<'a>(&'a mut self, ctx: &Context, total: usize) -> Element<'a, Message> { 118 | let col = Column::new() 119 | .push( 120 | Text::new("Canceling the movement of funds") 121 | .success() 122 | .bold() 123 | .size(50), 124 | ) 125 | .push( 126 | Text::new(&format!("{} vaults are revaulting", total)) 127 | .success() 128 | .bold(), 129 | ) 130 | .align_items(Alignment::Center) 131 | .spacing(30) 132 | .padding(20) 133 | .max_width(1000); 134 | 135 | self.modal.view( 136 | ctx, 137 | None, 138 | col, 139 | Some("By revaulting, you are broadcasting cancel transaction to the network\n, reverting the movement of funds back to the stakeholders wallet."), 140 | Message::Menu(Menu::Home), 141 | ).into() 142 | } 143 | } 144 | 145 | #[derive(Debug, Clone, Default)] 146 | pub struct RevaultVaultListItemView { 147 | select_button: iced::button::State, 148 | } 149 | 150 | impl RevaultVaultListItemView { 151 | pub fn view(&mut self, ctx: &Context, vault: &Vault, selected: bool) -> iced::Element { 152 | let content = Container::new( 153 | Row::new() 154 | .push( 155 | Container::new( 156 | Row::new() 157 | .push(if selected { 158 | badge::square_check() 159 | } else { 160 | badge::square() 161 | }) 162 | .spacing(20) 163 | .align_items(Alignment::Center), 164 | ) 165 | .width(Length::Fill), 166 | ) 167 | .push( 168 | Container::new(if selected { 169 | Row::new() 170 | .push( 171 | Text::new(&ctx.converter.converts(vault.amount)) 172 | .bold() 173 | .color(color::PRIMARY), 174 | ) 175 | .push( 176 | Text::new(&format!(" {}", ctx.converter.unit)) 177 | .small() 178 | .color(color::PRIMARY), 179 | ) 180 | .align_items(Alignment::Center) 181 | } else { 182 | Row::new() 183 | .push(Text::new(&ctx.converter.converts(vault.amount)).bold()) 184 | .push(Text::new(&format!(" {}", ctx.converter.unit)).small()) 185 | .align_items(Alignment::Center) 186 | }) 187 | .width(Length::Shrink), 188 | ) 189 | .spacing(20) 190 | .align_items(Alignment::Center), 191 | ); 192 | 193 | button::white_card_button(&mut self.select_button, content) 194 | .on_press(Message::SelectVault(outpoint(vault))) 195 | .into() 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/daemon/client/jsonrpc.rs: -------------------------------------------------------------------------------- 1 | // Rust JSON-RPC Library 2 | // Written by 3 | // Andrew Poelstra 4 | // Wladimir J. van der Laan 5 | // 6 | // To the extent possible under law, the author(s) have dedicated all 7 | // copyright and related and neighboring rights to this software to 8 | // the public domain worldwide. This software is distributed without 9 | // any warranty. 10 | // 11 | // You should have received a copy of the CC0 Public Domain Dedication 12 | // along with this software. 13 | // If not, see . 14 | // 15 | //! Client support 16 | //! 17 | //! Support for connecting to JSONRPC servers over UNIX socets, sending requests, 18 | //! and parsing responses 19 | //! 20 | 21 | #[cfg(windows)] 22 | use uds_windows::UnixStream; 23 | 24 | #[cfg(not(windows))] 25 | use std::os::unix::net::UnixStream; 26 | 27 | use std::fmt::Debug; 28 | use std::path::{Path, PathBuf}; 29 | use std::time::Duration; 30 | use std::{error, fmt, io}; 31 | 32 | use serde::de::DeserializeOwned; 33 | use serde::{Deserialize, Serialize}; 34 | use serde_json::{to_writer, Deserializer}; 35 | 36 | use log::debug; 37 | 38 | /// A handle to a remote JSONRPC server 39 | #[derive(Debug, Clone)] 40 | pub struct JsonRPCClient { 41 | sockpath: PathBuf, 42 | timeout: Option, 43 | } 44 | 45 | impl super::Client for JsonRPCClient { 46 | type Error = Error; 47 | fn request( 48 | &self, 49 | method: &str, 50 | params: Option, 51 | ) -> Result { 52 | self.send_request(method, params) 53 | .and_then(|res| res.into_result()) 54 | } 55 | } 56 | 57 | impl JsonRPCClient { 58 | /// Creates a new client 59 | pub fn new>(sockpath: P) -> JsonRPCClient { 60 | JsonRPCClient { 61 | sockpath: sockpath.as_ref().to_path_buf(), 62 | timeout: None, 63 | } 64 | } 65 | 66 | /// Set an optional timeout for requests 67 | #[allow(dead_code)] 68 | pub fn set_timeout(&mut self, timeout: Option) { 69 | self.timeout = timeout; 70 | } 71 | 72 | /// Sends a request to a client 73 | pub fn send_request( 74 | &self, 75 | method: &str, 76 | params: Option, 77 | ) -> Result, Error> { 78 | // Setup connection 79 | let mut stream = UnixStream::connect(&self.sockpath)?; 80 | stream.set_read_timeout(self.timeout)?; 81 | stream.set_write_timeout(self.timeout)?; 82 | 83 | let request = Request { 84 | method, 85 | params, 86 | id: std::process::id(), 87 | jsonrpc: "2.0", 88 | }; 89 | 90 | debug!("Sending to revaultd: {:#?}", request); 91 | 92 | to_writer(&mut stream, &request)?; 93 | 94 | let response: Response = Deserializer::from_reader(&mut stream) 95 | .into_iter() 96 | .next() 97 | .map_or(Err(Error::NoErrorOrResult), |res| Ok(res?))?; 98 | if response 99 | .jsonrpc 100 | .as_ref() 101 | .map_or(false, |version| version != "2.0") 102 | { 103 | return Err(Error::VersionMismatch); 104 | } 105 | 106 | if response.id != request.id { 107 | return Err(Error::NonceMismatch); 108 | } 109 | 110 | debug!("Received from revaultd: {:#?}", response); 111 | 112 | Ok(response) 113 | } 114 | } 115 | 116 | #[derive(Debug, Clone, PartialEq, Serialize)] 117 | /// A JSONRPC request object 118 | pub struct Request<'f, T: Serialize> { 119 | /// The name of the RPC call 120 | pub method: &'f str, 121 | /// Parameters to the RPC call 122 | pub params: Option, 123 | /// Identifier for this Request, which should appear in the response 124 | pub id: u32, 125 | /// jsonrpc field, MUST be "2.0" 126 | pub jsonrpc: &'f str, 127 | } 128 | 129 | #[derive(Debug, Clone, PartialEq, Deserialize)] 130 | /// A JSONRPC response object 131 | pub struct Response { 132 | /// A result if there is one, or null 133 | pub result: Option, 134 | /// An error if there is one, or null 135 | pub error: Option, 136 | /// Identifier for this Request, which should match that of the request 137 | pub id: u32, 138 | /// jsonrpc field, MUST be "2.0" 139 | pub jsonrpc: Option, 140 | } 141 | 142 | impl Response { 143 | /// Extract the result from a response, consuming the response 144 | pub fn into_result(self) -> Result { 145 | if let Some(e) = self.error { 146 | return Err(Error::Rpc(e)); 147 | } 148 | 149 | self.result.ok_or(Error::NoErrorOrResult) 150 | } 151 | 152 | /// Returns whether or not the `result` field is empty 153 | #[allow(dead_code)] 154 | pub fn is_none(&self) -> bool { 155 | self.result.is_none() 156 | } 157 | } 158 | 159 | #[allow(dead_code)] 160 | #[derive(Debug)] 161 | #[allow(non_camel_case_types)] 162 | pub enum RpcErrorCode { 163 | // Standard errors defined by JSON-RPC 2.0 standard 164 | /// Invalid request 165 | JSONRPC2_INVALID_REQUEST = -32600, 166 | /// Method not found 167 | JSONRPC2_METHOD_NOT_FOUND = -32601, 168 | /// Invalid parameters 169 | JSONRPC2_INVALID_PARAMS = -32602, 170 | } 171 | 172 | /// A library error 173 | #[derive(Debug)] 174 | pub enum Error { 175 | /// Json error 176 | Json(serde_json::Error), 177 | /// IO Error 178 | Io(io::Error), 179 | /// Error response 180 | Rpc(RpcError), 181 | /// Response has neither error nor result 182 | NoErrorOrResult, 183 | /// Response to a request did not have the expected nonce 184 | NonceMismatch, 185 | /// Response to a request had a jsonrpc field other than "2.0" 186 | VersionMismatch, 187 | } 188 | 189 | impl From for Error { 190 | fn from(e: serde_json::Error) -> Error { 191 | Error::Json(e) 192 | } 193 | } 194 | 195 | impl From for Error { 196 | fn from(e: io::Error) -> Error { 197 | Error::Io(e) 198 | } 199 | } 200 | 201 | impl From for Error { 202 | fn from(e: RpcError) -> Error { 203 | Error::Rpc(e) 204 | } 205 | } 206 | 207 | impl fmt::Display for Error { 208 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 209 | match *self { 210 | Error::Json(ref e) => write!(f, "JSON decode error: {}", e), 211 | Error::Io(ref e) => write!(f, "IO error response: {}", e), 212 | Error::Rpc(ref r) => write!(f, "RPC error response: {:?}", r), 213 | Error::NoErrorOrResult => write!(f, "Malformed RPC response"), 214 | Error::NonceMismatch => write!(f, "Nonce of response did not match nonce of request"), 215 | Error::VersionMismatch => write!(f, "`jsonrpc` field set to non-\"2.0\""), 216 | } 217 | } 218 | } 219 | 220 | impl error::Error for Error { 221 | fn cause(&self) -> Option<&dyn error::Error> { 222 | match *self { 223 | Error::Json(ref e) => Some(e), 224 | _ => None, 225 | } 226 | } 227 | } 228 | 229 | impl From for super::RevaultDError { 230 | fn from(e: Error) -> super::RevaultDError { 231 | match e { 232 | Error::Io(e) => super::RevaultDError::Transport(Some(e.kind()), format!("io: {:?}", e)), 233 | Error::Json(e) => super::RevaultDError::Transport(None, format!("json decode: {}", e)), 234 | Error::NonceMismatch => { 235 | super::RevaultDError::Transport(None, format!("transport: {}", e)) 236 | } 237 | Error::VersionMismatch => { 238 | super::RevaultDError::Transport(None, format!("transport: {}", e)) 239 | } 240 | Error::NoErrorOrResult => super::RevaultDError::NoAnswer, 241 | Error::Rpc(e) => super::RevaultDError::Rpc(e.code, e.message), 242 | } 243 | } 244 | } 245 | 246 | #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 247 | /// A JSONRPC error object 248 | pub struct RpcError { 249 | /// The integer identifier of the error 250 | pub code: i32, 251 | /// A string describing the error 252 | pub message: String, 253 | /// Additional data specific to the error 254 | pub data: Option, 255 | } 256 | -------------------------------------------------------------------------------- /ui/static/images/revault-colored-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /src/app/state/sign.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeMap, sync::Arc, time::Duration}; 2 | 3 | use bitcoin::{ 4 | blockdata::transaction::OutPoint, 5 | util::{ 6 | bip32::{Fingerprint, KeySource}, 7 | psbt::PartiallySignedTransaction as Psbt, 8 | }, 9 | Amount, PublicKey, 10 | }; 11 | use tokio::sync::Mutex; 12 | 13 | use iced::{time, Command, Element, Subscription}; 14 | 15 | use revault_hwi::{app::revault::RevaultHWI, HWIError}; 16 | 17 | use crate::{ 18 | app::{context::Context, error::Error, message::SignMessage, view::sign::SignerView}, 19 | daemon::model::{outpoint, Vault}, 20 | }; 21 | 22 | #[derive(Debug)] 23 | pub struct SpendTransactionTarget { 24 | pub spend_tx: Psbt, 25 | } 26 | 27 | impl SpendTransactionTarget { 28 | /// Creates a new SpendTransactionTarget to sign with only the corresponding keys of the given 29 | /// xpubs. The bip32_derivation of the psbt is filtered to possess only the given xpub 30 | /// fingerprints. 31 | pub fn new(fingerprints: &Vec, mut spend_tx: Psbt) -> Self { 32 | for input in &mut spend_tx.inputs { 33 | let mut new_derivation: BTreeMap = BTreeMap::new(); 34 | for (key, source) in &input.bip32_derivation { 35 | if fingerprints.contains(&source.0) { 36 | new_derivation.insert(*key, source.clone()); 37 | } 38 | } 39 | input.bip32_derivation = new_derivation; 40 | } 41 | Self { spend_tx } 42 | } 43 | } 44 | 45 | #[derive(Debug)] 46 | pub struct Signer { 47 | device: Device, 48 | processing: bool, 49 | signed: bool, 50 | 51 | pub error: Option, 52 | pub target: T, 53 | 54 | view: SignerView, 55 | } 56 | 57 | impl Signer { 58 | pub fn new(target: T) -> Self { 59 | Signer { 60 | device: Device::new(), 61 | processing: false, 62 | signed: false, 63 | error: None, 64 | target, 65 | view: SignerView::new(), 66 | } 67 | } 68 | 69 | pub fn signed(&self) -> bool { 70 | self.signed 71 | } 72 | 73 | pub fn subscription(&self) -> Subscription { 74 | if !self.signed && !self.processing { 75 | self.device.subscription() 76 | } else { 77 | Subscription::none() 78 | } 79 | } 80 | 81 | pub fn view(&mut self, ctx: &Context) -> Element { 82 | self.view.view( 83 | ctx, 84 | self.device.is_connected(), 85 | self.processing, 86 | self.signed, 87 | ) 88 | } 89 | } 90 | 91 | impl Signer { 92 | pub fn update(&mut self, ctx: &Context, message: SignMessage) -> Command { 93 | match message { 94 | SignMessage::SelectSign => { 95 | self.processing = true; 96 | return Command::perform( 97 | self.device 98 | .clone() 99 | .sign_spend_tx(self.target.spend_tx.clone()), 100 | |tx| SignMessage::PsbtSigned(tx.map(Box::new)), 101 | ); 102 | } 103 | SignMessage::PsbtSigned(res) => { 104 | self.processing = false; 105 | match res { 106 | Ok(tx) => { 107 | if tx.global.unsigned_tx.txid() 108 | == self.target.spend_tx.global.unsigned_tx.txid() 109 | { 110 | let user_manager_xpub = 111 | ctx.config.daemon.manager_config.as_ref().unwrap().xpub; 112 | for input in &tx.inputs { 113 | if !input.partial_sigs.keys().any(|key| { 114 | input 115 | .bip32_derivation 116 | .get(key) 117 | .map(|(fingerprint, _)| { 118 | user_manager_xpub.fingerprint() == *fingerprint 119 | }) 120 | .unwrap_or(false) 121 | }) { 122 | log::info!("Hardware wallet did not sign the spend tx"); 123 | self.error = Some(HWIError::DeviceDidNotSign.into()); 124 | return Command::none(); 125 | } 126 | } 127 | self.signed = true; 128 | self.target.spend_tx = *tx; 129 | } 130 | } 131 | Err(e) => { 132 | log::info!("{:?}", e); 133 | self.error = Some(e.into()); 134 | } 135 | } 136 | } 137 | _ => return self.device.update(&ctx, message), 138 | }; 139 | Command::none() 140 | } 141 | } 142 | 143 | #[derive(Debug, Clone)] 144 | pub struct Device { 145 | channel: Option>>>, 146 | } 147 | 148 | impl Device { 149 | pub fn new() -> Self { 150 | Device { channel: None } 151 | } 152 | 153 | pub fn is_connected(&self) -> bool { 154 | self.channel.is_some() 155 | } 156 | 157 | pub fn update(&mut self, ctx: &Context, message: SignMessage) -> Command { 158 | match message { 159 | SignMessage::Ping(res) => { 160 | if res.is_err() { 161 | self.channel = None; 162 | } 163 | } 164 | SignMessage::CheckConnection => { 165 | if let Some(channel) = self.channel.clone() { 166 | return Command::perform( 167 | async move { channel.lock().await.is_connected().await }, 168 | SignMessage::Ping, 169 | ); 170 | } else { 171 | let connect = &ctx.hardware_wallet; 172 | return Command::perform(connect(), |res| { 173 | SignMessage::Connected(res.map(|channel| Arc::new(Mutex::new(channel)))) 174 | }); 175 | } 176 | } 177 | SignMessage::Connected(Ok(channel)) => self.channel = Some(channel), 178 | _ => {} 179 | }; 180 | Command::none() 181 | } 182 | 183 | pub fn subscription(&self) -> Subscription { 184 | time::every(Duration::from_secs(1)).map(|_| SignMessage::CheckConnection) 185 | } 186 | 187 | pub async fn sign_revocation_txs( 188 | self, 189 | emergency_tx: Psbt, 190 | emergency_unvault_tx: Psbt, 191 | cancel_txs: [Psbt; 5], 192 | ) -> Result<(Psbt, Psbt, [Psbt; 5]), HWIError> { 193 | if let Some(channel) = self.channel { 194 | channel 195 | .lock() 196 | .await 197 | .sign_revocation_txs(&emergency_tx, &emergency_unvault_tx, &cancel_txs) 198 | .await 199 | } else { 200 | Err(HWIError::DeviceDisconnected) 201 | } 202 | } 203 | 204 | pub async fn sign_unvault_tx(self, unvault_tx: Psbt) -> Result { 205 | if let Some(channel) = self.channel { 206 | channel.lock().await.sign_unvault_tx(&unvault_tx).await 207 | } else { 208 | Err(HWIError::DeviceDisconnected) 209 | } 210 | } 211 | 212 | pub async fn sign_spend_tx(self, spend_tx: Psbt) -> Result { 213 | if let Some(channel) = self.channel { 214 | let mut res = channel.lock().await; 215 | return res.sign_tx(&spend_tx).await; 216 | } else { 217 | Err(HWIError::DeviceDisconnected) 218 | } 219 | } 220 | 221 | pub async fn secure_batch( 222 | self, 223 | deposits: &Vec, 224 | ) -> Result, HWIError> { 225 | if let Some(channel) = self.channel { 226 | let utxos: Vec<(OutPoint, Amount, u32)> = deposits 227 | .iter() 228 | .map(|deposit| { 229 | ( 230 | outpoint(deposit), 231 | deposit.amount, 232 | deposit.derivation_index.into(), 233 | ) 234 | }) 235 | .collect(); 236 | channel.lock().await.create_vaults(&utxos).await 237 | } else { 238 | Err(HWIError::DeviceDisconnected) 239 | } 240 | } 241 | 242 | pub async fn delegate_batch(self, vaults: &Vec) -> Result, HWIError> { 243 | if let Some(channel) = self.channel { 244 | let utxos: Vec<(OutPoint, Amount, u32)> = vaults 245 | .iter() 246 | .map(|vault| (outpoint(vault), vault.amount, vault.derivation_index.into())) 247 | .collect(); 248 | channel.lock().await.delegate_vaults(&utxos).await 249 | } else { 250 | Err(HWIError::DeviceDisconnected) 251 | } 252 | } 253 | } 254 | --------------------------------------------------------------------------------