Merge #1241: [GUI] Installer - Add Electrum node option

819eb920c0210b9d95689f40cfbeba65c03280af gui(settings): allow to change node type (Michael Mallan)
2381227216663a6a2336ee79905646fb2a3aadca gui(settings): view & edit Electrum settings (Michael Mallan)
b570039ff8e740be43c6aebdc62910f4ab4eac05 gui(settings): rename Bitcoin Core to Node (Michael Mallan)
db20ae4b677cf4ffdf19ad0df6f50ea983b34a02 gui(installer): reduce empty space height (Michael Mallan)
0993905879629c58da24a1145acbb49faf31da3a gui(installer): update wording to include Electrum (Michael Mallan)
f40af570bccecf9361410df334ad30e45f57659b gui(installer): split long string and run cargo fmt (Michael Mallan)
0f09be151ca3ba1353d23b170805c845fc8ab5cc gui(installer): don't change values while waiting for ping result (Michael Mallan)
c93aa88d74620a555bd442bc3504e12af198f00a gui(installer): add electrum node option (Michael Mallan)
341e4467dbf727d78f31690b8dbc405995ab4088 gui(installer): allow for different node types (Michael Mallan)
83172c7bc584283d7dd4b93f3f18ec0f2d9a1ad0 gui(installer): add general node definition (Michael Mallan)
046b54e6a9337aaf478ea6e61f2d28d73f1b15c1 gui(installer): define bitcoind from general node struct (Michael Mallan)
c5d9d007fb908308592635b8f85236a1b7a4b5ae gui: move bitcoind to new node module (Michael Mallan)
4536eff561459648cbf0666ec757db95ac29de4f gui(installer): extract logic for try ping bitcoind (Michael Mallan)
ef44cf329adc0eed01451950454edfb7e54be515 gui(installer): add module for node step (Michael Mallan)
f74f071b8a3e4eda77cc5b468f891b21c5e4e2a4 gui: upgrade liana dependency (jp1ac4)

Pull request description:

  This is for #1223.

  For now, it's possible to edit the node's settings but not to change node type.

  Remaining tasks:

  - [x] Revert Cargo.toml once #1222 is merged.
  - [x] Update wording as per https://github.com/wizardsardine/liana/issues/1223#issuecomment-2286483134.

ACKs for top commit:
  pythcoiner:
    ACK 819eb920c0210b9d95689f40cfbeba65c03280af

Tree-SHA512: 362a14d32c2e13ba286d252d9f8a1106d63e5c40198776653b0623b433435329663126307e17da017fdbbd8a8ad273b703cc3ba54fd13fa5a0afd7dd9179089a
This commit is contained in:
Antoine Poinsot 2024-09-06 11:15:57 +02:00
commit 55958c2a75
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
19 changed files with 1163 additions and 320 deletions

69
gui/Cargo.lock generated
View File

@ -308,12 +308,32 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bdk_chain"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c601c4dc7e6c3efa538a0afbb43b964cefab9a9b5e8f352fa0ca38145448a5e7"
dependencies = [
"bitcoin",
"miniscript",
]
[[package]]
name = "bdk_coin_select"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c084bf76f0f67546fc814ffa82044144be1bb4618183a15016c162f8b087ad4"
[[package]]
name = "bdk_electrum"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28906275aeb1f71dc32045670f06c8a26fb17cc62151a99f7425d258f4bda589"
dependencies = [
"bdk_chain",
"electrum-client",
]
[[package]]
name = "bech32"
version = "0.10.0-beta"
@ -1143,11 +1163,11 @@ dependencies = [
[[package]]
name = "dirs"
version = "5.0.0"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dece029acd3353e3a58ac2e3eb3c8d6c35827a892edc6cc4138ef9c33df46ecd"
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
dependencies = [
"dirs-sys 0.4.0",
"dirs-sys 0.4.1",
]
[[package]]
@ -1163,13 +1183,14 @@ dependencies = [
[[package]]
name = "dirs-sys"
version = "0.4.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04414300db88f70d74c5ff54e50f9e1d1737d9a5b90f53fcf2e95ca2a9ab554b"
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.45.0",
"windows-sys 0.48.0",
]
[[package]]
@ -1267,6 +1288,23 @@ version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
[[package]]
name = "electrum-client"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89008f106be6f303695522f2f4c1f28b40c3e8367ed8b3bb227f1f882cb52cc2"
dependencies = [
"bitcoin",
"byteorder",
"libc",
"log",
"rustls",
"serde",
"serde_json",
"webpki-roots",
"winapi",
]
[[package]]
name = "elliptic-curve"
version = "0.13.8"
@ -2589,12 +2627,13 @@ dependencies = [
[[package]]
name = "liana"
version = "6.0.0"
source = "git+https://github.com/wizardsardine/liana?branch=master#585bb5b763127f4e0686ce8201fb846fa84137b9"
source = "git+https://github.com/wizardsardine/liana?branch=master#6f7334738360a554d17875b364ccdf2120250315"
dependencies = [
"backtrace",
"bdk_coin_select",
"bdk_electrum",
"bip39",
"dirs 5.0.0",
"dirs 5.0.1",
"fern",
"getrandom",
"jsonrpc 0.17.0",
@ -2936,9 +2975,9 @@ dependencies = [
[[package]]
name = "minreq"
version = "2.8.1"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3de406eeb24aba36ed3829532fa01649129677186b44a49debec0ec574ca7da7"
checksum = "763d142cdff44aaadd9268bebddb156ef6c65a0e13486bb81673cf2d8739f9b0"
dependencies = [
"log",
"serde",
@ -3260,6 +3299,12 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "orbclient"
version = "0.3.47"
@ -3818,9 +3863,9 @@ checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f"
[[package]]
name = "rdrand"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e233b642160555c1aa1ff7a78443c6139342f411b6fa6602af2ebbfee9e166bb"
checksum = "d92195228612ac8eed47adbc2ed0f04e513a4ccb98175b6f2bd04d963b533655"
dependencies = [
"rand_core",
]

View File

@ -35,8 +35,8 @@ use state::{
use crate::{
app::{cache::Cache, error::Error, menu::Menu, wallet::Wallet},
bitcoind::Bitcoind,
daemon::{embedded::EmbeddedDaemon, Daemon, DaemonBackend},
node::bitcoind::Bitcoind,
};
use self::state::SettingsState;

View File

@ -1,5 +1,5 @@
use std::convert::{From, TryInto};
use std::net::SocketAddr;
use std::net::{SocketAddr, SocketAddrV4};
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
@ -9,7 +9,9 @@ use iced::Command;
use tracing::info;
use liana::{
config::{BitcoinConfig, BitcoindConfig, BitcoindRpcAuth, Config},
config::{
BitcoinBackend, BitcoinConfig, BitcoindConfig, BitcoindRpcAuth, Config, ElectrumConfig,
},
miniscript::bitcoin::Network,
};
@ -17,8 +19,11 @@ use liana_ui::{component::form, widget::Element};
use crate::{
app::{cache::Cache, error::Error, message::Message, state::settings::State, view},
bitcoind::{RpcAuthType, RpcAuthValues},
daemon::Daemon,
node::{
bitcoind::{RpcAuthType, RpcAuthValues},
NodeType,
},
};
#[derive(Debug)]
@ -27,6 +32,7 @@ pub struct BitcoindSettingsState {
config_updated: bool,
node_settings: Option<BitcoindSettings>,
electrum_settings: Option<ElectrumSettings>,
rescan_settings: RescanSetting,
}
@ -37,17 +43,52 @@ impl BitcoindSettingsState {
daemon_is_external: bool,
bitcoind_is_internal: bool,
) -> Self {
let mut configured_node_type = None;
let (bitcoind_config, electrum_config) =
match config.clone().and_then(|c| c.bitcoin_backend) {
Some(BitcoinBackend::Bitcoind(bitcoind_config)) => {
configured_node_type = Some(NodeType::Bitcoind);
let dummy_electrum = ElectrumConfig {
addr: String::default(),
};
(Some(bitcoind_config), Some(dummy_electrum))
}
Some(BitcoinBackend::Electrum(electrum_config)) => {
configured_node_type = Some(NodeType::Electrum);
// The dummy values will be ignored.
let dummy_bitcoind = BitcoindConfig {
addr: SocketAddr::V4(SocketAddrV4::from_str("127.0.0.1:10000").unwrap()),
rpc_auth: BitcoindRpcAuth::CookieFile(PathBuf::from_str("").unwrap()),
};
(Some(dummy_bitcoind), Some(electrum_config))
}
_ => (None, None),
};
BitcoindSettingsState {
warning: None,
config_updated: false,
node_settings: config.map(|config| {
node_settings: bitcoind_config.map(|bitcoind_config| {
BitcoindSettings::new(
config.bitcoin_config.clone(),
config.bitcoind_config.unwrap(),
configured_node_type,
config
.clone()
.expect("config must exist if bitcoind_config exists")
.bitcoin_config,
bitcoind_config,
daemon_is_external,
bitcoind_is_internal,
)
}),
electrum_settings: electrum_config.map(|electrum_config| {
ElectrumSettings::new(
configured_node_type,
config
.expect("config must exist if electrum_config exists")
.bitcoin_config,
electrum_config,
daemon_is_external,
)
}),
rescan_settings: RescanSetting::new(cache.rescan_progress),
}
}
@ -73,6 +114,14 @@ impl State for BitcoindSettingsState {
))
});
}
if let Some(settings) = &mut self.electrum_settings {
settings.edited(true);
return Command::perform(async {}, |_| {
Message::View(view::Message::Settings(
view::SettingsMessage::EditBitcoindSettings,
))
});
}
}
Err(e) => {
self.config_updated = false;
@ -101,6 +150,13 @@ impl State for BitcoindSettingsState {
return settings.update(daemon, cache, msg);
}
}
Message::View(view::Message::Settings(view::SettingsMessage::ElectrumSettings(
msg,
))) => {
if let Some(settings) = &mut self.electrum_settings {
return settings.update(daemon, cache, msg);
}
}
Message::View(view::Message::Settings(view::SettingsMessage::RescanSettings(msg))) => {
return self.rescan_settings.update(daemon, cache, msg);
}
@ -110,9 +166,17 @@ impl State for BitcoindSettingsState {
}
fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> {
let can_edit_bitcoind_settings = !self.rescan_settings.processing;
let can_do_rescan = !self.rescan_settings.processing
&& self.node_settings.as_ref().map(|settings| settings.edit) != Some(true);
let can_edit_bitcoind_settings =
self.node_settings.is_some() && !self.rescan_settings.processing;
let can_edit_electrum_settings =
self.electrum_settings.is_some() && !self.rescan_settings.processing;
let settings_edit = self.node_settings.as_ref().map(|settings| settings.edit) == Some(true)
|| self
.electrum_settings
.as_ref()
.map(|settings| settings.edit)
== Some(true);
let can_do_rescan = !self.rescan_settings.processing && !settings_edit;
view::settings::bitcoind_settings(
cache,
self.warning.as_ref(),
@ -123,6 +187,13 @@ impl State for BitcoindSettingsState {
.map(move |msg| {
view::Message::Settings(view::SettingsMessage::BitcoindSettings(msg))
}),
self.electrum_settings
.as_ref()
.expect("If we have bitcoind, we must also have electrum")
.view(cache, can_edit_electrum_settings)
.map(move |msg| {
view::Message::Settings(view::SettingsMessage::ElectrumSettings(msg))
}),
self.rescan_settings
.view(cache, can_do_rescan)
.map(move |msg| {
@ -149,6 +220,7 @@ impl From<BitcoindSettingsState> for Box<dyn State> {
#[derive(Debug)]
pub struct BitcoindSettings {
configured_node_type: Option<NodeType>,
bitcoind_config: BitcoindConfig,
bitcoin_config: BitcoinConfig,
edit: bool,
@ -162,6 +234,7 @@ pub struct BitcoindSettings {
impl BitcoindSettings {
fn new(
configured_node_type: Option<NodeType>,
bitcoin_config: BitcoinConfig,
bitcoind_config: BitcoindConfig,
daemon_is_external: bool,
@ -194,8 +267,13 @@ impl BitcoindSettings {
RpcAuthType::UserPass,
),
};
let addr = bitcoind_config.addr.to_string();
let addr = if configured_node_type == Some(NodeType::Bitcoind) {
bitcoind_config.addr.to_string()
} else {
String::default()
};
BitcoindSettings {
configured_node_type,
daemon_is_external,
bitcoind_is_internal,
bitcoind_config,
@ -274,10 +352,11 @@ impl BitcoindSettings {
if let (true, Some(rpc_auth)) = (self.addr.valid, rpc_auth) {
let mut daemon_config = daemon.config().cloned().unwrap();
daemon_config.bitcoind_config = Some(liana::config::BitcoindConfig {
rpc_auth,
addr: new_addr.unwrap(),
});
daemon_config.bitcoin_backend =
Some(liana::config::BitcoinBackend::Bitcoind(BitcoindConfig {
rpc_auth,
addr: new_addr.unwrap(),
}));
self.processing = true;
return Command::perform(async move { daemon_config }, |cfg| {
Message::LoadDaemonConfig(Box::new(cfg))
@ -289,8 +368,10 @@ impl BitcoindSettings {
}
fn view<'a>(&self, cache: &'a Cache, can_edit: bool) -> Element<'a, view::SettingsEditMessage> {
let is_configured_node_type = self.configured_node_type == Some(NodeType::Bitcoind);
if self.edit {
view::settings::bitcoind_edit(
is_configured_node_type,
self.bitcoin_config.network,
cache.blockheight,
&self.addr,
@ -300,6 +381,7 @@ impl BitcoindSettings {
)
} else {
view::settings::bitcoind(
is_configured_node_type,
self.bitcoin_config.network,
&self.bitcoind_config,
cache.blockheight,
@ -310,6 +392,111 @@ impl BitcoindSettings {
}
}
#[derive(Debug)]
pub struct ElectrumSettings {
configured_node_type: Option<NodeType>,
electrum_config: ElectrumConfig,
bitcoin_config: BitcoinConfig,
edit: bool,
processing: bool,
addr: form::Value<String>,
daemon_is_external: bool,
}
impl ElectrumSettings {
fn new(
configured_node_type: Option<NodeType>,
bitcoin_config: BitcoinConfig,
electrum_config: ElectrumConfig,
daemon_is_external: bool,
) -> ElectrumSettings {
let addr = electrum_config.addr.to_string();
ElectrumSettings {
configured_node_type,
daemon_is_external,
electrum_config,
bitcoin_config,
edit: false,
processing: false,
addr: form::Value {
valid: true,
value: addr,
},
}
}
}
impl ElectrumSettings {
fn edited(&mut self, success: bool) {
self.processing = false;
if success {
self.edit = false;
}
}
fn update(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
_cache: &Cache,
message: view::SettingsEditMessage,
) -> Command<Message> {
match message {
view::SettingsEditMessage::Select => {
if !self.processing {
self.edit = true;
}
}
view::SettingsEditMessage::Cancel => {
if !self.processing {
self.edit = false;
}
}
view::SettingsEditMessage::FieldEdited(field, value) => {
if !self.processing && field == "address" {
self.addr.value = value
}
}
view::SettingsEditMessage::Confirm => {
if self.addr.valid {
let mut daemon_config = daemon.config().cloned().unwrap();
daemon_config.bitcoin_backend =
Some(liana::config::BitcoinBackend::Electrum(ElectrumConfig {
addr: self.addr.value.clone(),
}));
self.processing = true;
return Command::perform(async move { daemon_config }, |cfg| {
Message::LoadDaemonConfig(Box::new(cfg))
});
}
}
_ => {}
};
Command::none()
}
fn view<'a>(&self, cache: &'a Cache, can_edit: bool) -> Element<'a, view::SettingsEditMessage> {
let is_configured_node_type = self.configured_node_type == Some(NodeType::Electrum);
if self.edit {
view::settings::electrum_edit(
is_configured_node_type,
self.bitcoin_config.network,
cache.blockheight,
&self.addr,
self.processing,
)
} else {
view::settings::electrum(
is_configured_node_type,
self.bitcoin_config.network,
&self.electrum_config,
cache.blockheight,
Some(cache.blockheight != 0),
can_edit && !self.daemon_is_external,
)
}
}
}
#[derive(Debug, Default)]
pub struct RescanSetting {
processing: bool,

View File

@ -1,4 +1,4 @@
use crate::{app::menu::Menu, bitcoind::RpcAuthType};
use crate::{app::menu::Menu, node::bitcoind::RpcAuthType};
use liana::miniscript::bitcoin::bip32::Fingerprint;
#[derive(Debug, Clone)]
@ -67,6 +67,7 @@ pub enum SpendTxMessage {
pub enum SettingsMessage {
EditBitcoindSettings,
BitcoindSettings(SettingsEditMessage),
ElectrumSettings(SettingsEditMessage),
RescanSettings(SettingsEditMessage),
EditRemoteBackendSettings,
RemoteBackendSettings(RemoteBackendSettingsMessage),

View File

@ -29,8 +29,8 @@ use crate::{
menu::Menu,
view::{hw, warning::warn},
},
bitcoind::{RpcAuthType, RpcAuthValues},
hw::HardwareWallet,
node::bitcoind::{RpcAuthType, RpcAuthValues},
};
pub fn list(cache: &Cache, is_remote_backend: bool) -> Element<Message> {
@ -51,7 +51,7 @@ pub fn list(cache: &Cache, is_remote_backend: bool) -> Element<Message> {
Button::new(
Row::new()
.push(badge::Badge::new(icon::bitcoin_icon()))
.push(text("Bitcoin Core").bold())
.push(text("Node").bold())
.padding(10)
.spacing(20)
.align_items(Alignment::Center)
@ -161,7 +161,7 @@ pub fn bitcoind_settings<'a>(
)
.push(icon::chevron_right().size(30))
.push(
Button::new(text("Bitcoin Core").size(30).bold())
Button::new(text("Node").size(30).bold())
.style(theme::Button::Transparent)
.on_press(Message::Settings(SettingsMessage::EditBitcoindSettings)),
),
@ -298,6 +298,7 @@ pub fn remote_backend_section<'a>(
}
pub fn bitcoind_edit<'a>(
is_configured_node_type: bool,
network: Network,
blockheight: i32,
addr: &form::Value<String>,
@ -306,7 +307,7 @@ pub fn bitcoind_edit<'a>(
processing: bool,
) -> Element<'a, SettingsEditMessage> {
let mut col = Column::new().spacing(20);
if blockheight != 0 {
if is_configured_node_type && blockheight != 0 {
col = col
.push(
Row::new()
@ -444,6 +445,7 @@ pub fn bitcoind_edit<'a>(
}
pub fn bitcoind<'a>(
is_configured_node_type: bool,
network: Network,
config: &liana::config::BitcoindConfig,
blockheight: i32,
@ -451,7 +453,7 @@ pub fn bitcoind<'a>(
can_edit: bool,
) -> Element<'a, SettingsEditMessage> {
let mut col = Column::new().spacing(20);
if blockheight != 0 {
if is_configured_node_type && blockheight != 0 {
col = col
.push(
Row::new()
@ -482,16 +484,18 @@ pub fn bitcoind<'a>(
}
let mut rows = vec![];
match &config.rpc_auth {
BitcoindRpcAuth::CookieFile(path) => {
rows.push(("Cookie file path:", path.to_str().unwrap().to_string()));
}
BitcoindRpcAuth::UserPass(user, password) => {
rows.push(("User:", user.clone()));
rows.push(("Password:", password.clone()));
if is_configured_node_type {
match &config.rpc_auth {
BitcoindRpcAuth::CookieFile(path) => {
rows.push(("Cookie file path:", path.to_str().unwrap().to_string()));
}
BitcoindRpcAuth::UserPass(user, password) => {
rows.push(("User:", user.clone()));
rows.push(("Password:", password.clone()));
}
}
rows.push(("Socket address:", config.addr.to_string()));
}
rows.push(("Socket address:", config.addr.to_string()));
let mut col_fields = Column::new();
for (k, v) in rows {
@ -510,7 +514,188 @@ pub fn bitcoind<'a>(
Row::new()
.push(badge::Badge::new(icon::bitcoin_icon()))
.push(text("Bitcoin Core").bold())
.push(is_running_label(is_running))
.push_maybe(if is_configured_node_type {
Some(is_running_label(is_running))
} else {
None
})
.spacing(20)
.align_items(Alignment::Center)
.width(Length::Fill),
)
.push(if can_edit {
Button::new(icon::pencil_icon())
.style(theme::Button::TransparentBorder)
.on_press(SettingsEditMessage::Select)
} else {
Button::new(icon::pencil_icon()).style(theme::Button::TransparentBorder)
})
.align_items(Alignment::Center),
)
.push(separation().width(Length::Fill))
.push(col.push(col_fields))
.spacing(20),
))
.width(Length::Fill)
.into()
}
pub fn electrum_edit<'a>(
is_configured_node_type: bool,
network: Network,
blockheight: i32,
addr: &form::Value<String>,
processing: bool,
) -> Element<'a, SettingsEditMessage> {
let mut col = Column::new().spacing(20);
if is_configured_node_type && blockheight != 0 {
col = col
.push(
Row::new()
.push(
Row::new()
.push(badge::Badge::new(icon::network_icon()))
.push(
Column::new()
.push(text("Network:"))
.push(text(network.to_string()).bold()),
)
.spacing(10)
.width(Length::FillPortion(1)),
)
.push(
Row::new()
.push(badge::Badge::new(icon::block_icon()))
.push(
Column::new()
.push(text("Block Height:"))
.push(text(blockheight.to_string()).bold()),
)
.spacing(10)
.width(Length::FillPortion(1)),
),
)
.push(separation().width(Length::Fill));
}
col = col.push(
Column::new()
.push(text("Address:").bold().small())
.push(
form::Form::new_trimmed("127:0.0.1:50001", addr, |value| {
SettingsEditMessage::FieldEdited("address", value)
})
.warning("Please enter a valid address")
.size(P1_SIZE)
.padding(5),
)
.spacing(5),
);
let mut cancel_button = button::transparent(None, " Cancel ").padding(5);
let mut confirm_button = button::primary(None, " Save ").padding(5);
if !processing {
cancel_button = cancel_button.on_press(SettingsEditMessage::Cancel);
confirm_button = confirm_button.on_press(SettingsEditMessage::Confirm);
}
card::simple(Container::new(
Column::new()
.push(
Row::new()
.push(badge::Badge::new(icon::bitcoin_icon()))
.push(text("Electrum").bold())
.padding(10)
.spacing(20)
.align_items(Alignment::Center)
.width(Length::Fill),
)
.push(separation().width(Length::Fill))
.push(col)
.push(
Container::new(
Row::new()
.push(cancel_button)
.push(confirm_button)
.spacing(10)
.align_items(Alignment::Center),
)
.width(Length::Fill)
.align_x(alignment::Horizontal::Right),
)
.spacing(20),
))
.width(Length::Fill)
.into()
}
pub fn electrum<'a>(
is_configured_node_type: bool,
network: Network,
config: &liana::config::ElectrumConfig,
blockheight: i32,
is_running: Option<bool>,
can_edit: bool,
) -> Element<'a, SettingsEditMessage> {
let mut col = Column::new().spacing(20);
if is_configured_node_type && blockheight != 0 {
col = col
.push(
Row::new()
.push(
Row::new()
.push(badge::Badge::new(icon::network_icon()))
.push(
Column::new()
.push(text("Network:"))
.push(text(network.to_string()).bold()),
)
.spacing(10)
.width(Length::FillPortion(1)),
)
.push(
Row::new()
.push(badge::Badge::new(icon::block_icon()))
.push(
Column::new()
.push(text("Block Height:"))
.push(text(blockheight.to_string()).bold()),
)
.spacing(10)
.width(Length::FillPortion(1)),
),
)
.push(separation().width(Length::Fill));
}
let rows = if is_configured_node_type {
vec![("Address:", config.addr.to_string())]
} else {
vec![]
};
let mut col_fields = Column::new();
for (k, v) in rows {
col_fields = col_fields.push(
Row::new()
.push(Container::new(text(k).bold().small()).width(Length::Fill))
.push(text(v).small()),
);
}
card::simple(Container::new(
Column::new()
.push(
Row::new()
.push(
Row::new()
.push(badge::Badge::new(icon::bitcoin_icon()))
.push(text("Electrum").bold())
.push_maybe(if is_configured_node_type {
Some(is_running_label(is_running))
} else {
None
})
.spacing(20)
.align_items(Alignment::Center)
.width(Length::Fill),

View File

@ -27,9 +27,9 @@ impl EmbeddedDaemon {
pub async fn command<T, F>(&self, method: F) -> Result<T, DaemonError>
where
F: FnOnce(&DaemonControl) -> Result<T, DaemonError>,
F: FnOnce(&mut DaemonControl) -> Result<T, DaemonError>,
{
match self.handle.lock().await.as_ref() {
match self.handle.lock().await.as_mut() {
Some(DaemonHandle::Controller { control, .. }) => method(control),
None => Err(DaemonError::DaemonStopped),
}

View File

@ -4,13 +4,13 @@ use std::time::Duration;
use crate::{
app::settings::KeySetting,
bitcoind::{Bitcoind, InternalBitcoindConfig},
lianalite::client::backend::{BackendClient, BackendWalletClient},
node::bitcoind::{Bitcoind, InternalBitcoindConfig},
signer::Signer,
};
use async_hwi::DeviceKind;
use liana::{
config::{BitcoinConfig, BitcoindConfig},
config::{BitcoinBackend, BitcoinConfig},
descriptors::LianaDescriptor,
miniscript::bitcoin,
};
@ -48,7 +48,7 @@ impl RemoteBackend {
#[derive(Clone)]
pub struct Context {
pub bitcoin_config: BitcoinConfig,
pub bitcoind_config: Option<BitcoindConfig>,
pub bitcoin_backend: Option<BitcoinBackend>,
pub descriptor: Option<LianaDescriptor>,
pub keys: Vec<KeySetting>,
pub hws: Vec<(DeviceKind, bitcoin::bip32::Fingerprint, Option<[u8; 32]>)>,
@ -77,7 +77,7 @@ impl Context {
},
hws: Vec::new(),
keys: Vec::new(),
bitcoind_config: None,
bitcoin_backend: None,
descriptor: None,
data_dir,
network,

View File

@ -6,10 +6,13 @@ use std::path::PathBuf;
use super::{context, Error};
use crate::{
bitcoind::{Bitcoind, ConfigField, RpcAuthType},
download::Progress,
hw::HardwareWalletMessage,
lianalite::client::{auth::AuthClient, backend::api},
node::{
bitcoind::{Bitcoind, ConfigField, RpcAuthType},
electrum, NodeType,
},
};
use async_hwi::{DeviceKind, Version};
@ -33,7 +36,7 @@ pub enum Message {
ImportRemoteWallet(ImportRemoteWallet),
SelectBitcoindType(SelectBitcoindTypeMsg),
InternalBitcoind(InternalBitcoindMsg),
DefineBitcoind(DefineBitcoind),
DefineNode(DefineNode),
DefineDescriptor(DefineDescriptor),
ImportXpub(Fingerprint, Result<DescriptorPublicKey, Error>),
HardwareWallets(HardwareWalletMessage),
@ -72,8 +75,20 @@ pub enum ImportRemoteWallet {
pub enum DefineBitcoind {
ConfigFieldEdited(ConfigField, String),
RpcAuthTypeSelected(RpcAuthType),
PingBitcoindResult(Result<(), Error>),
PingBitcoind,
}
#[derive(Debug, Clone)]
pub enum DefineElectrum {
ConfigFieldEdited(electrum::ConfigField, String),
}
#[derive(Debug, Clone)]
pub enum DefineNode {
NodeTypeSelected(NodeType),
DefineBitcoind(DefineBitcoind),
DefineElectrum(DefineElectrum),
PingResult((NodeType, Result<(), Error>)),
Ping,
}
#[derive(Debug, Clone)]

View File

@ -39,7 +39,7 @@ use crate::{
pub use message::Message;
use step::{
BackupDescriptor, BackupMnemonic, ChooseBackend, DefineBitcoind, DefineDescriptor, Final,
BackupDescriptor, BackupMnemonic, ChooseBackend, DefineDescriptor, DefineNode, Final,
ImportDescriptor, ImportRemoteWallet, InternalBitcoindStep, RecoverMnemonic,
RegisterDescriptor, RemoteBackendLogin, SelectBitcoindTypeStep, ShareXpubs, Step,
};
@ -126,7 +126,7 @@ impl Installer {
RemoteBackendLogin::new(network).into(),
SelectBitcoindTypeStep::new().into(),
InternalBitcoindStep::new(&context.data_dir).into(),
DefineBitcoind::new().into(),
DefineNode::default().into(),
Final::new().into(),
],
UserFlow::ShareXpubs => vec![ShareXpubs::new(network, signer.clone()).into()],
@ -139,7 +139,7 @@ impl Installer {
RegisterDescriptor::new_import_wallet().into(),
SelectBitcoindTypeStep::new().into(),
InternalBitcoindStep::new(&context.data_dir).into(),
DefineBitcoind::new().into(),
DefineNode::default().into(),
Final::new().into(),
],
},
@ -689,7 +689,7 @@ pub fn extract_daemon_config(ctx: &Context) -> Config {
.expect("Context must have a descriptor at this point"),
data_dir: Some(ctx.data_dir.clone()),
bitcoin_config: ctx.bitcoin_config.clone(),
bitcoind_config: ctx.bitcoind_config.clone(),
bitcoin_backend: ctx.bitcoin_backend.clone(),
}
}
@ -701,6 +701,7 @@ pub enum Error {
Backend(Arc<DaemonError>),
Settings(SettingsError),
Bitcoind(String),
Electrum(String),
CannotCreateDatadir(String),
CannotCreateFile(String),
CannotWriteToFile(String),
@ -752,6 +753,7 @@ impl std::fmt::Display for Error {
Self::Backend(e) => write!(f, "Remote backend error: {}", e),
Self::Settings(e) => write!(f, "Settings file error: {}", e),
Self::Bitcoind(e) => write!(f, "Failed to ping bitcoind: {}", e),
Self::Electrum(e) => write!(f, "Failed to ping Electrum: {}", e),
Self::CannotCreateDatadir(e) => write!(f, "Failed to create datadir: {}", e),
Self::CannotGetAvailablePort(e) => write!(f, "Failed to get available port: {}", e),
Self::CannotWriteToFile(e) => write!(f, "Failed to write to file: {}", e),

View File

@ -1,11 +1,12 @@
mod backend;
mod bitcoind;
mod descriptor;
mod mnemonic;
mod node;
mod share_xpubs;
pub use bitcoind::{
DefineBitcoind, DownloadState, InstallState, InternalBitcoindStep, SelectBitcoindTypeStep,
pub use node::{
bitcoind::{DownloadState, InstallState, InternalBitcoindStep, SelectBitcoindTypeStep},
DefineNode,
};
pub use descriptor::{BackupDescriptor, DefineDescriptor, ImportDescriptor, RegisterDescriptor};
@ -21,9 +22,9 @@ use iced::{Command, Subscription};
use liana_ui::widget::*;
use crate::{
bitcoind::Bitcoind,
hw::HardwareWallets,
installer::{context::Context, message::Message, view},
node::bitcoind::Bitcoind,
};
pub trait Step {

View File

@ -9,7 +9,7 @@ use bitcoin_hashes::{sha256, Hash};
use flate2::read::GzDecoder;
use iced::{Command, Subscription};
use liana::{
config::{BitcoindConfig, BitcoindRpcAuth},
config::{BitcoinBackend, BitcoindConfig, BitcoindRpcAuth},
miniscript::bitcoin::Network,
};
#[cfg(any(target_os = "macos", target_os = "linux"))]
@ -21,12 +21,6 @@ use jsonrpc::{client::Client, simple_http::SimpleHttpTransport};
use liana_ui::{component::form, widget::*};
use crate::{
bitcoind::{
self, bitcoind_network_dir, internal_bitcoind_datadir, internal_bitcoind_directory,
Bitcoind, ConfigField, InternalBitcoindConfig, InternalBitcoindConfigError,
InternalBitcoindNetworkConfig, RpcAuth, RpcAuthType, RpcAuthValues,
StartInternalBitcoindError, VERSION,
},
download,
hw::HardwareWallets,
installer::{
@ -35,6 +29,12 @@ use crate::{
step::Step,
view, Error,
},
node::bitcoind::{
self, bitcoind_network_dir, internal_bitcoind_datadir, internal_bitcoind_directory,
Bitcoind, ConfigField, InternalBitcoindConfig, InternalBitcoindConfigError,
InternalBitcoindNetworkConfig, RpcAuth, RpcAuthType, RpcAuthValues,
StartInternalBitcoindError, VERSION,
},
};
// The approach for tracking download progress is taken from
@ -320,7 +320,7 @@ impl Step for SelectBitcoindTypeStep {
fn apply(&mut self, ctx: &mut Context) -> bool {
if !self.use_external {
if ctx.internal_bitcoind_config.is_none() {
ctx.bitcoind_config = None; // Ensures internal bitcoind can be restarted in case user has switched selection.
ctx.bitcoin_backend = None; // Ensures internal bitcoind can be restarted in case user has switched selection.
}
} else {
ctx.internal_bitcoind_config = None;
@ -339,11 +339,11 @@ impl Step for SelectBitcoindTypeStep {
}
}
#[derive(Clone)]
pub struct DefineBitcoind {
rpc_auth_vals: RpcAuthValues,
selected_auth_type: RpcAuthType,
address: form::Value<String>,
is_running: Option<Result<(), Error>>,
// Internal cache to detect network change.
network: Option<Network>,
@ -355,47 +355,46 @@ impl DefineBitcoind {
rpc_auth_vals: RpcAuthValues::default(),
selected_auth_type: RpcAuthType::CookieFile,
address: form::Value::default(),
is_running: None,
network: None,
}
}
pub fn ping(&self) -> Command<Message> {
let address = self.address.value.to_owned();
let selected_auth_type = self.selected_auth_type;
pub fn ping(&self) -> Result<(), Error> {
let rpc_auth_vals = self.rpc_auth_vals.clone();
Command::perform(
async move {
let builder = match selected_auth_type {
RpcAuthType::CookieFile => {
let cookie_path = rpc_auth_vals.cookie_path.value;
let cookie = std::fs::read_to_string(cookie_path).map_err(|e| {
Error::Bitcoind(format!("Failed to read cookie file: {}", e))
})?;
SimpleHttpTransport::builder().cookie_auth(cookie)
}
RpcAuthType::UserPass => {
let user = rpc_auth_vals.user.value;
let password = rpc_auth_vals.password.value;
SimpleHttpTransport::builder().auth(user, Some(password))
}
};
let client = Client::with_transport(
builder
.url(&address)?
.timeout(std::time::Duration::from_secs(3))
.build(),
);
client.send_request(client.build_request("echo", &[]))?;
Ok(())
},
|res| Message::DefineBitcoind(message::DefineBitcoind::PingBitcoindResult(res)),
)
let builder = match self.selected_auth_type {
RpcAuthType::CookieFile => {
let cookie_path = rpc_auth_vals.cookie_path.value;
let cookie = std::fs::read_to_string(cookie_path)
.map_err(|e| Error::Bitcoind(format!("Failed to read cookie file: {}", e)))?;
SimpleHttpTransport::builder().cookie_auth(cookie)
}
RpcAuthType::UserPass => {
let user = rpc_auth_vals.user.value;
let password = rpc_auth_vals.password.value;
SimpleHttpTransport::builder().auth(user, Some(password))
}
};
let client = Client::with_transport(
builder
.url(&self.address.value.to_owned())?
.timeout(std::time::Duration::from_secs(3))
.build(),
);
client.send_request(client.build_request("echo", &[]))?;
Ok(())
}
}
impl Step for DefineBitcoind {
fn load_context(&mut self, ctx: &Context) {
pub fn can_try_ping(&self) -> bool {
if let RpcAuthType::UserPass = self.selected_auth_type {
self.address.valid
&& !self.rpc_auth_vals.password.value.is_empty()
&& !self.rpc_auth_vals.user.value.is_empty()
} else {
self.address.valid && !self.rpc_auth_vals.cookie_path.value.is_empty()
}
}
pub fn load_context(&mut self, ctx: &Context) {
if self.rpc_auth_vals.cookie_path.value.is_empty()
// if network changed then the values must be reset to default.
|| self.network != Some(ctx.bitcoin_config.network)
@ -412,17 +411,12 @@ impl Step for DefineBitcoind {
self.network = Some(ctx.bitcoin_config.network);
}
fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command<Message> {
if let Message::DefineBitcoind(msg) = message {
pub fn update(&mut self, message: message::DefineNode) -> Command<Message> {
if let message::DefineNode::DefineBitcoind(msg) = message {
match msg {
message::DefineBitcoind::PingBitcoind => {
self.is_running = None;
return self.ping();
}
message::DefineBitcoind::PingBitcoindResult(res) => self.is_running = Some(res),
message::DefineBitcoind::ConfigFieldEdited(field, value) => match field {
ConfigField::Address => {
self.is_running = None;
self.address.value.clone_from(&value);
self.address.valid = false;
if let Some((ip, port)) = value.rsplit_once(':') {
@ -434,23 +428,19 @@ impl Step for DefineBitcoind {
}
}
ConfigField::CookieFilePath => {
self.is_running = None;
self.rpc_auth_vals.cookie_path.value = value;
self.rpc_auth_vals.cookie_path.valid = true;
}
ConfigField::User => {
self.is_running = None;
self.rpc_auth_vals.user.value = value;
self.rpc_auth_vals.user.valid = true;
}
ConfigField::Password => {
self.is_running = None;
self.rpc_auth_vals.password.value = value;
self.rpc_auth_vals.password.valid = true;
}
},
message::DefineBitcoind::RpcAuthTypeSelected(auth_type) => {
self.is_running = None;
self.selected_auth_type = auth_type;
}
};
@ -458,7 +448,7 @@ impl Step for DefineBitcoind {
Command::none()
}
fn apply(&mut self, ctx: &mut Context) -> bool {
pub fn apply(&mut self, ctx: &mut Context) -> bool {
let addr = std::net::SocketAddr::from_str(&self.address.value);
let rpc_auth = match self.selected_auth_type {
RpcAuthType::CookieFile => {
@ -481,33 +471,18 @@ impl Step for DefineBitcoind {
false
}
(Some(rpc_auth), Ok(addr)) => {
ctx.bitcoind_config = Some(BitcoindConfig { rpc_auth, addr });
ctx.bitcoin_backend =
Some(liana::config::BitcoinBackend::Bitcoind(BitcoindConfig {
rpc_auth,
addr,
}));
true
}
}
}
fn view(
&self,
_hws: &HardwareWallets,
progress: (usize, usize),
_email: Option<&str>,
) -> Element<Message> {
view::define_bitcoin(
progress,
&self.address,
&self.rpc_auth_vals,
&self.selected_auth_type,
self.is_running.as_ref(),
)
}
fn load(&self) -> Command<Message> {
self.ping()
}
fn skip(&self, ctx: &Context) -> bool {
!ctx.bitcoind_is_external || ctx.remote_backend.is_some()
pub fn view(&self) -> Element<Message> {
view::define_bitcoind(&self.address, &self.rpc_auth_vals, &self.selected_auth_type)
}
}
@ -517,12 +492,6 @@ impl Default for DefineBitcoind {
}
}
impl From<DefineBitcoind> for Box<dyn Step> {
fn from(s: DefineBitcoind) -> Box<dyn Step> {
Box::new(s)
}
}
pub struct InternalBitcoindStep {
liana_datadir: PathBuf,
bitcoind_datadir: PathBuf,
@ -579,7 +548,7 @@ impl Step for InternalBitcoindStep {
}
if let Some(Ok(_)) = self.started {
// This case can arise if a user switches from internal bitcoind to external and back to internal.
if ctx.bitcoind_config.is_none() {
if ctx.bitcoin_backend.is_none() {
self.started = None; // So that internal bitcoind will be restarted.
}
}
@ -790,7 +759,10 @@ impl Step for InternalBitcoindStep {
fn apply(&mut self, ctx: &mut Context) -> bool {
// Any errors have been handled as part of `message::InternalBitcoindMsg::Start`
if let Some(Ok(_)) = self.started {
ctx.bitcoind_config.clone_from(&self.bitcoind_config);
ctx.bitcoin_backend = self
.bitcoind_config
.as_ref()
.map(|bitcoind_config| BitcoinBackend::Bitcoind(bitcoind_config.clone()));
ctx.internal_bitcoind_config
.clone_from(&self.internal_bitcoind_config);
ctx.internal_bitcoind.clone_from(&self.internal_bitcoind);

View File

@ -0,0 +1,86 @@
use iced::Command;
use liana::{
config::ElectrumConfig,
electrum_client::{self, ElectrumApi},
};
use liana_ui::{component::form, widget::*};
use crate::{
installer::{
context::Context,
message::{self, Message},
view, Error,
},
node::electrum::ConfigField,
};
#[derive(Clone)]
pub struct DefineElectrum {
address: form::Value<String>,
}
impl DefineElectrum {
pub fn new() -> Self {
Self {
address: form::Value::default(),
}
}
pub fn can_try_ping(&self) -> bool {
!self.address.value.is_empty() && self.address.valid
}
pub fn update(&mut self, message: message::DefineNode) -> Command<Message> {
if let message::DefineNode::DefineElectrum(msg) = message {
match msg {
message::DefineElectrum::ConfigFieldEdited(field, value) => match field {
ConfigField::Address => {
let value_noprefix = if value.starts_with("ssl://") {
value.replacen("ssl://", "", 1)
} else {
value.replacen("tcp://", "", 1)
};
let noprefix_parts: Vec<_> = value_noprefix.split(':').collect();
self.address.value.clone_from(&value); // save the value including any prefix
self.address.valid = noprefix_parts.len() == 2
&& !noprefix_parts
.first()
.expect("there are two parts")
.is_empty()
&& noprefix_parts
.last()
.expect("there are two parts")
.parse::<u16>() // check it is a port
.is_ok();
}
},
};
};
Command::none()
}
pub fn apply(&mut self, ctx: &mut Context) -> bool {
if self.can_try_ping() {
ctx.bitcoin_backend = Some(liana::config::BitcoinBackend::Electrum(ElectrumConfig {
addr: self.address.value.clone(),
}));
return true;
}
false
}
pub fn view(&self) -> Element<Message> {
view::define_electrum(&self.address)
}
pub fn ping(&self) -> Result<(), Error> {
let builder = electrum_client::Config::builder();
let config = builder.timeout(Some(3)).build();
let client = electrum_client::Client::from_config(&self.address.value, config)
.map_err(|e| Error::Electrum(e.to_string()))?;
client
.raw_call("server.ping", [])
.map_err(|e| Error::Electrum(e.to_string()))?;
Ok(())
}
}

View File

@ -0,0 +1,253 @@
pub mod bitcoind;
pub mod electrum;
use crate::{
hw::HardwareWallets,
installer::{
context::Context,
message::{self, Message},
step::{
node::{bitcoind::DefineBitcoind, electrum::DefineElectrum},
Step,
},
view, Error,
},
node::NodeType,
};
use iced::Command;
use liana_ui::widget::Element;
#[derive(Clone)]
pub enum NodeDefinition {
Bitcoind(DefineBitcoind),
Electrum(DefineElectrum),
}
impl NodeDefinition {
fn new(node_type: NodeType) -> Self {
match node_type {
NodeType::Bitcoind => NodeDefinition::Bitcoind(DefineBitcoind::new()),
NodeType::Electrum => NodeDefinition::Electrum(DefineElectrum::new()),
}
}
fn node_type(&self) -> NodeType {
match self {
NodeDefinition::Bitcoind(_) => NodeType::Bitcoind,
NodeDefinition::Electrum(_) => NodeType::Electrum,
}
}
fn apply(&mut self, ctx: &mut Context) -> bool {
match self {
NodeDefinition::Bitcoind(def) => def.apply(ctx),
NodeDefinition::Electrum(def) => def.apply(ctx),
}
}
fn can_try_ping(&self) -> bool {
match self {
NodeDefinition::Bitcoind(def) => def.can_try_ping(),
NodeDefinition::Electrum(def) => def.can_try_ping(),
}
}
fn load_context(&mut self, ctx: &Context) {
match self {
NodeDefinition::Bitcoind(def) => def.load_context(ctx),
NodeDefinition::Electrum(_) => {
// noop for now
}
}
}
fn update(&mut self, message: message::DefineNode) -> Command<Message> {
match self {
NodeDefinition::Bitcoind(def) => def.update(message),
NodeDefinition::Electrum(def) => def.update(message),
}
}
fn view(&self) -> Element<Message> {
match self {
NodeDefinition::Bitcoind(def) => def.view(),
NodeDefinition::Electrum(def) => def.view(),
}
}
fn ping(&self) -> Result<(), Error> {
match self {
NodeDefinition::Bitcoind(def) => def.ping(),
NodeDefinition::Electrum(def) => def.ping(),
}
}
}
pub struct Node {
definition: NodeDefinition,
is_running: Option<Result<(), Error>>,
waiting_for_ping_result: bool,
}
impl Node {
fn new(node_type: NodeType) -> Self {
Node {
definition: NodeDefinition::new(node_type),
is_running: None,
waiting_for_ping_result: false,
}
}
}
pub struct DefineNode {
nodes: Vec<Node>,
selected_node_type: NodeType,
}
impl From<DefineNode> for Box<dyn Step> {
fn from(s: DefineNode) -> Box<dyn Step> {
Box::new(s)
}
}
impl DefineNode {
pub fn new(selected_node_type: NodeType) -> Self {
let available_node_types = [
// This is the order in which the available node types will be shown to the user.
NodeType::Bitcoind,
NodeType::Electrum,
];
assert!(available_node_types.contains(&selected_node_type));
let nodes = available_node_types
.iter()
.copied()
.map(Node::new)
.collect();
Self {
nodes,
selected_node_type,
}
}
pub fn selected_mut(&mut self) -> &mut Node {
self.get_mut(self.selected_node_type)
.expect("selected type must be present")
}
pub fn selected(&self) -> &Node {
self.get(self.selected_node_type)
.expect("selected type must be present")
}
pub fn get_mut(&mut self, node_type: NodeType) -> Option<&mut Node> {
self.nodes
.iter_mut()
.find(|node| node.definition.node_type() == node_type)
}
pub fn get(&self, node_type: NodeType) -> Option<&Node> {
self.nodes
.iter()
.find(|node| node.definition.node_type() == node_type)
}
fn update_node(
&mut self,
node_type: NodeType,
message: message::DefineNode,
) -> Command<Message> {
if let Some(node) = self.get_mut(node_type) {
// Don't make changes while waiting for a ping result so that we
// know which values the ping result applies to.
if !node.waiting_for_ping_result {
node.is_running = None;
return node.definition.update(message);
}
}
Command::none()
}
}
impl Default for DefineNode {
fn default() -> Self {
Self::new(NodeType::Bitcoind)
}
}
impl Step for DefineNode {
fn load_context(&mut self, ctx: &Context) {
for node in self.nodes.iter_mut() {
node.definition.load_context(ctx);
}
}
fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command<Message> {
if let Message::DefineNode(msg) = message {
match msg {
message::DefineNode::NodeTypeSelected(node_type) => {
self.selected_node_type = node_type;
}
message::DefineNode::Ping => {
let selected = self.selected_mut();
// Make sure we don't send more than one ping request at a time
// so that we know which values the result applies to.
if !selected.waiting_for_ping_result {
selected.waiting_for_ping_result = true;
selected.is_running = None;
let def = selected.definition.clone();
let node_type = def.node_type();
return Command::perform(async move { def.ping() }, move |res| {
Message::DefineNode(message::DefineNode::PingResult((node_type, res)))
});
}
}
message::DefineNode::PingResult((node_type, res)) => {
// Result may not be for the selected node type.
if let Some(node) = self.get_mut(node_type) {
// Make sure we're expecting the ping result. Otherwise, the user may have changed values
// and so the ping result may not apply to the current values.
if node.waiting_for_ping_result {
node.waiting_for_ping_result = false;
node.is_running = Some(res);
}
}
}
msg @ message::DefineNode::DefineBitcoind(_) => {
return self.update_node(NodeType::Bitcoind, msg);
}
msg @ message::DefineNode::DefineElectrum(_) => {
return self.update_node(NodeType::Electrum, msg);
}
}
}
Command::none()
}
fn apply(&mut self, ctx: &mut Context) -> bool {
self.selected_mut().definition.apply(ctx)
}
fn view(
&self,
_hws: &HardwareWallets,
progress: (usize, usize),
_email: Option<&str>,
) -> Element<Message> {
// TODO: Make input fields read-only while waiting for a ping result.
view::define_bitcoin_node(
progress,
self.nodes.iter().map(|node| node.definition.node_type()),
self.selected_node_type,
self.selected().definition.view(),
self.selected().is_running.as_ref(),
self.selected().definition.can_try_ping(),
self.selected().waiting_for_ping_result,
)
}
fn skip(&self, ctx: &Context) -> bool {
!ctx.bitcoind_is_external || ctx.remote_backend.is_some()
}
}

View File

@ -32,14 +32,17 @@ use liana_ui::{
};
use crate::{
bitcoind::{ConfigField, RpcAuthType, RpcAuthValues, StartInternalBitcoindError},
hw::{is_compatible_with_tapminiscript, HardwareWallet, UnsupportedReason},
installer::{
message::{self, Message},
message::{self, DefineBitcoind, DefineNode, Message},
prompt,
step::{DownloadState, InstallState},
Error,
},
node::{
bitcoind::{ConfigField, RpcAuthType, RpcAuthValues, StartInternalBitcoindError},
electrum, NodeType,
},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -524,11 +527,12 @@ pub fn import_descriptor<'a>(
"Import the wallet",
Column::new()
.push(Column::new().spacing(20).push(col_descriptor).push(text(
"After creating the wallet, \
you will need to perform a rescan of \
the blockchain in order to see your \
coins and past transactions. This can \
be done in Settings > Bitcoin Core.",
"If you are using a Bitcoin Core node, \
you will need to perform a rescan of \
the blockchain after creating the wallet \
in order to see your coins and past \
transactions. This can be done in \
Settings > Node.",
)))
.push(
if imported_descriptor.value.is_empty() || !imported_descriptor.valid {
@ -1159,12 +1163,107 @@ pub fn help_backup<'a>() -> Element<'a, Message> {
text(prompt::BACKUP_DESCRIPTOR_HELP).small().into()
}
pub fn define_bitcoin<'a>(
pub fn define_bitcoin_node<'a>(
progress: (usize, usize),
available_node_types: impl Iterator<Item = NodeType>,
selected_node_type: NodeType,
node_view: Element<'a, Message>,
is_running: Option<&Result<(), Error>>,
can_try_ping: bool,
waiting_for_ping_result: bool,
) -> Element<'a, Message> {
let col = Column::new()
.push(
available_node_types.fold(
Row::new()
.push(text("Node type:").small().bold())
.spacing(10),
|row, node_type| {
row.push(radio(
match node_type {
NodeType::Bitcoind => "Bitcoin Core",
NodeType::Electrum => "Electrum",
},
node_type,
Some(selected_node_type),
|new_selection| {
Message::DefineNode(message::DefineNode::NodeTypeSelected(
new_selection,
))
},
))
.spacing(30)
.align_items(Alignment::Center)
},
),
)
.push(node_view)
.push_maybe(if waiting_for_ping_result {
Some(Container::new(
Row::new()
.spacing(10)
.align_items(Alignment::Center)
.push(text("Checking connection...")),
))
} else if is_running.is_some() {
is_running.map(|res| {
if res.is_ok() {
Container::new(
Row::new()
.spacing(10)
.align_items(Alignment::Center)
.push(icon::circle_check_icon().style(color::GREEN))
.push(text("Connection checked").style(color::GREEN)),
)
} else {
Container::new(
Row::new()
.spacing(10)
.align_items(Alignment::Center)
.push(icon::circle_cross_icon().style(color::RED))
.push(text("Connection failed").style(color::RED)),
)
}
})
} else {
Some(Container::new(Space::with_height(Length::Fixed(21.0))))
})
.push(
Row::new()
.spacing(10)
.push(Container::new(
button::secondary(None, "Check connection")
.on_press_maybe(if can_try_ping && !waiting_for_ping_result {
Some(Message::DefineNode(DefineNode::Ping))
} else {
None
})
.width(Length::Fixed(200.0)),
))
.push(if is_running.map(|res| res.is_ok()).unwrap_or(false) {
button::primary(None, "Next")
.on_press(Message::Next)
.width(Length::Fixed(200.0))
} else {
button::primary(None, "Next").width(Length::Fixed(200.0))
}),
)
.spacing(50);
layout(
progress,
None,
"Set up connection to the Bitcoin node",
col,
true,
Some(Message::Previous),
)
}
pub fn define_bitcoind<'a>(
address: &form::Value<String>,
rpc_auth_vals: &RpcAuthValues,
selected_auth_type: &RpcAuthType,
is_running: Option<&Result<(), Error>>,
) -> Element<'a, Message> {
let is_loopback = if let Some((ip, _port)) = address.value.clone().rsplit_once(':') {
let (ipv4, ipv6) = (Ipv4Addr::from_str(ip), Ipv6Addr::from_str(ip));
@ -1181,9 +1280,8 @@ pub fn define_bitcoin<'a>(
.push(text("Address:").bold())
.push(
form::Form::new_trimmed("Address", address, |msg| {
Message::DefineBitcoind(message::DefineBitcoind::ConfigFieldEdited(
ConfigField::Address,
msg,
Message::DefineNode(DefineNode::DefineBitcoind(
DefineBitcoind::ConfigFieldEdited(ConfigField::Address, msg),
))
})
.warning("Please enter correct address")
@ -1219,9 +1317,9 @@ pub fn define_bitcoin<'a>(
*auth_type,
Some(*selected_auth_type),
|new_selection| {
Message::DefineBitcoind(
message::DefineBitcoind::RpcAuthTypeSelected(new_selection),
)
Message::DefineNode(DefineNode::DefineBitcoind(
DefineBitcoind::RpcAuthTypeSelected(new_selection),
))
},
))
.spacing(30)
@ -1232,9 +1330,8 @@ pub fn define_bitcoin<'a>(
.push(match selected_auth_type {
RpcAuthType::CookieFile => Row::new().push(
form::Form::new_trimmed("Cookie path", &rpc_auth_vals.cookie_path, |msg| {
Message::DefineBitcoind(message::DefineBitcoind::ConfigFieldEdited(
ConfigField::CookieFilePath,
msg,
Message::DefineNode(DefineNode::DefineBitcoind(
DefineBitcoind::ConfigFieldEdited(ConfigField::CookieFilePath, msg),
))
})
.warning("Please enter correct path")
@ -1244,9 +1341,8 @@ pub fn define_bitcoin<'a>(
RpcAuthType::UserPass => Row::new()
.push(
form::Form::new_trimmed("User", &rpc_auth_vals.user, |msg| {
Message::DefineBitcoind(message::DefineBitcoind::ConfigFieldEdited(
ConfigField::User,
msg,
Message::DefineNode(DefineNode::DefineBitcoind(
DefineBitcoind::ConfigFieldEdited(ConfigField::User, msg),
))
})
.warning("Please enter correct user")
@ -1255,9 +1351,8 @@ pub fn define_bitcoin<'a>(
)
.push(
form::Form::new_trimmed("Password", &rpc_auth_vals.password, |msg| {
Message::DefineBitcoind(message::DefineBitcoind::ConfigFieldEdited(
ConfigField::Password,
msg,
Message::DefineNode(DefineNode::DefineBitcoind(
DefineBitcoind::ConfigFieldEdited(ConfigField::Password, msg),
))
})
.warning("Please enter correct password")
@ -1268,69 +1363,32 @@ pub fn define_bitcoin<'a>(
})
.spacing(10);
let check_connect_enable = if let RpcAuthType::UserPass = selected_auth_type {
address.valid
&& !rpc_auth_vals.password.value.is_empty()
&& !rpc_auth_vals.user.value.is_empty()
} else {
address.valid && !rpc_auth_vals.cookie_path.value.is_empty()
};
layout(
progress,
None,
"Set up connection to the Bitcoin full node",
Column::new()
.push(col_address)
.push(col_auth)
.push_maybe(if is_running.is_some() {
is_running.map(|res| {
if res.is_ok() {
Container::new(
Row::new()
.spacing(10)
.align_items(Alignment::Center)
.push(icon::circle_check_icon().style(color::GREEN))
.push(text("Connection checked").style(color::GREEN)),
)
} else {
Container::new(
Row::new()
.spacing(10)
.align_items(Alignment::Center)
.push(icon::circle_cross_icon().style(color::RED))
.push(text("Connection failed").style(color::RED)),
)
}
})
} else {
Some(Container::new(Space::with_height(Length::Fixed(25.0))))
Column::new()
.push(col_address)
.push(col_auth)
.spacing(50)
.into()
}
pub fn define_electrum<'a>(address: &form::Value<String>) -> Element<'a, Message> {
let col_address = Column::new()
.push(text("Address:").bold())
.push(
form::Form::new_trimmed("127.0.0.1:50001", address, |msg| {
Message::DefineNode(DefineNode::DefineElectrum(
message::DefineElectrum::ConfigFieldEdited(electrum::ConfigField::Address, msg),
))
})
.push(
Row::new()
.spacing(10)
.push(Container::new(
button::secondary(None, "Check connection")
.on_press_maybe(if check_connect_enable {
Some(Message::DefineBitcoind(
message::DefineBitcoind::PingBitcoind,
))
} else {
None
})
.width(Length::Fixed(200.0)),
))
.push(if is_running.map(|res| res.is_ok()).unwrap_or(false) {
button::primary(None, "Next")
.on_press(Message::Next)
.width(Length::Fixed(200.0))
} else {
button::primary(None, "Next").width(Length::Fixed(200.0))
}),
.warning(
"Please enter correct address (including port), \
optionally prefixed with tcp:// or ssl://",
)
.spacing(50),
true,
Some(Message::Previous),
)
.size(text::P1_SIZE)
.padding(10),
)
.spacing(10);
Column::new().push(col_address).spacing(50).into()
}
pub fn select_bitcoind_type<'a>(progress: (usize, usize)) -> Element<'a, Message> {
@ -1338,91 +1396,107 @@ pub fn select_bitcoind_type<'a>(progress: (usize, usize)) -> Element<'a, Message
progress,
None,
"Bitcoin node management",
Column::new().push(
Row::new()
.align_items(Alignment::Start)
.spacing(20)
.push(
Container::new(
Column::new()
.spacing(20)
.width(Length::Fixed(300.0))
.push(text("Manage your own Bitcoin node").bold())
)
.padding(20),
)
.push(
Container::new(
Column::new()
.spacing(20)
.width(Length::Fixed(300.0))
.push(text("Have Liana manage and run a dedicated Bitcoin node").bold())
)
.padding(20),
),
)
.push(
Row::new()
.align_items(Alignment::Start)
.spacing(20)
.push(
Container::new(
Column::new()
.spacing(20)
.width(Length::Fixed(300.0))
.align_items(Alignment::Start)
.push(text("Liana will connect to your existing instance of Bitcoin Core. You will have to make sure Bitcoin Core is running when you use Liana.\n\n(Use this if you already have a full node on your machine, and don't need a new instance)"))
)
.padding(20),
)
.push(
Container::new(
Column::new()
.spacing(20)
.width(Length::Fixed(300.0))
.align_items(Alignment::Start)
.push(text("Liana will run its own instance of Bitcoin Core. This will use a pruned node, and perform the synchronization in the Liana folder.\n\nIf you select this option, Bitcoin Core will be downloaded, installed and started on the next step.\n\n(Use this if you don't want to deal with Bitcoin Core yourself, or need a new, dedicated instance for Liana)"))
)
.padding(20),
),
)
.push(
Row::new()
.align_items(Alignment::End)
.spacing(20)
.push(
Container::new(
Column::new()
.spacing(20)
.width(Length::Fixed(300.0))
.align_items(Alignment::Center)
.push(
button::primary(None, "Select")
Column::new()
.push(
Row::new()
.align_items(Alignment::Start)
.spacing(20)
.push(
Container::new(
Column::new()
.spacing(20)
.width(Length::Fixed(300.0))
.on_press(Message::SelectBitcoindType(
message::SelectBitcoindTypeMsg::UseExternal(true),
)),
)
.push(text("I already have a node").bold()),
)
.padding(20),
)
.padding(20),
)
.push(
Container::new(
Column::new()
.spacing(20)
.width(Length::Fixed(300.0))
.align_items(Alignment::Center)
.push(
button::primary(None, "Select")
.width(Length::Fixed(300.0))
.on_press(Message::SelectBitcoindType(
message::SelectBitcoindTypeMsg::UseExternal(false),
)),
)
.push(
Container::new(
Column::new().spacing(20).width(Length::Fixed(300.0)).push(
text(
"I want Liana to automatically install \
a Bitcoin node on my device",
)
.bold(),
),
)
.padding(20),
),
)
.push(
Row::new()
.align_items(Alignment::Start)
.spacing(20)
.push(
Container::new(
Column::new()
.spacing(20)
.width(Length::Fixed(300.0))
.align_items(Alignment::Start)
.push(text(
"Select this option if you already have \
a Bitcoin node running locally or remotely. \
Liana will connect to it.",
)),
)
.padding(20),
)
.padding(20),
),
),
.push(
Container::new(
Column::new()
.spacing(20)
.width(Length::Fixed(300.0))
.align_items(Alignment::Start)
.push(text(
"Liana will install a pruned node \
on your computer. You won't need to do anything \
except have some disk space available \
(~30GB required on mainnet) and \
wait for the initial synchronization with the \
network (it can take some days depending on \
your internet connection speed).",
)),
)
.padding(20),
),
)
.push(
Row::new()
.align_items(Alignment::End)
.spacing(20)
.push(
Container::new(
Column::new()
.spacing(20)
.width(Length::Fixed(300.0))
.align_items(Alignment::Center)
.push(
button::primary(None, "Select")
.width(Length::Fixed(300.0))
.on_press(Message::SelectBitcoindType(
message::SelectBitcoindTypeMsg::UseExternal(true),
)),
),
)
.padding(20),
)
.push(
Container::new(
Column::new()
.spacing(20)
.width(Length::Fixed(300.0))
.align_items(Alignment::Center)
.push(
button::primary(None, "Select")
.width(Length::Fixed(300.0))
.on_press(Message::SelectBitcoindType(
message::SelectBitcoindTypeMsg::UseExternal(false),
)),
),
)
.padding(20),
),
),
true,
Some(Message::Previous),
)
@ -1436,7 +1510,7 @@ pub fn start_internal_bitcoind<'a>(
download_state: Option<&DownloadState>,
install_state: Option<&InstallState>,
) -> Element<'a, Message> {
let version = crate::bitcoind::VERSION;
let version = crate::node::bitcoind::VERSION;
let mut next_button = button::primary(None, "Next").width(Length::Fixed(200.0));
if let Some(Ok(_)) = started {
next_button = next_button.on_press(Message::Next);

View File

@ -1,5 +1,4 @@
pub mod app;
pub mod bitcoind;
pub mod daemon;
pub mod datadir;
pub mod download;
@ -9,6 +8,7 @@ pub mod launcher;
pub mod lianalite;
pub mod loader;
pub mod logger;
pub mod node;
pub mod signer;
pub mod utils;

View File

@ -11,7 +11,7 @@ use tracing::{debug, info, warn};
use liana::{
commands::CoinStatus,
config::{Config, ConfigError},
config::{BitcoinBackend, Config, ConfigError},
miniscript::bitcoin,
StartupError,
};
@ -29,10 +29,10 @@ use crate::{
config::Config as GUIConfig,
wallet::{Wallet, WalletError},
},
bitcoind::{
daemon::{client, embedded::EmbeddedDaemon, model::*, Daemon, DaemonError},
node::bitcoind::{
internal_bitcoind_debug_log_path, stop_bitcoind, Bitcoind, StartInternalBitcoindError,
},
daemon::{client, embedded::EmbeddedDaemon, model::*, Daemon, DaemonError},
};
const SYNCING_PROGRESS_1: &str = "Bitcoin Core is synchronising the blockchain. A full synchronisation typically take a few days, and is resource intensive. Once the initial synchronisation is done, the next ones will be much faster.";
@ -245,7 +245,7 @@ impl Loader {
log::info!("Managed bitcoind stopped.");
} else if self.waiting_daemon_bitcoind && self.gui_config.start_internal_bitcoind {
if let Ok(config) = Config::from_file(self.gui_config.daemon_config_path.clone()) {
if let Some(bitcoind_config) = &config.bitcoind_config {
if let Some(BitcoinBackend::Bitcoind(bitcoind_config)) = &config.bitcoin_backend {
let mut retry = 0;
while !stop_bitcoind(bitcoind_config) && retry < 10 {
std::thread::sleep(std::time::Duration::from_millis(500));
@ -500,7 +500,7 @@ pub async fn start_bitcoind_and_daemon(
let config = Config::from_file(Some(config_path)).map_err(Error::Config)?;
let mut bitcoind: Option<Bitcoind> = None;
if start_internal_bitcoind {
if let Some(bitcoind_config) = &config.bitcoind_config {
if let Some(BitcoinBackend::Bitcoind(bitcoind_config)) = &config.bitcoin_backend {
// Check if bitcoind is already running before trying to start it.
if liana::BitcoinD::new(bitcoind_config, "internal_bitcoind_start".to_string()).is_ok()
{

14
gui/src/node/electrum.rs Normal file
View File

@ -0,0 +1,14 @@
use std::fmt;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum ConfigField {
Address,
}
impl fmt::Display for ConfigField {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ConfigField::Address => write!(f, "RPC address"),
}
}
}

8
gui/src/node/mod.rs Normal file
View File

@ -0,0 +1,8 @@
pub mod bitcoind;
pub mod electrum;
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
pub enum NodeType {
Bitcoind,
Electrum,
}