Split into lianad and liana crate
This commit is contained in:
parent
36b24d4d81
commit
74820d97e9
@ -75,7 +75,7 @@ task:
|
|||||||
fingerprint_script:
|
fingerprint_script:
|
||||||
- rustc --version
|
- rustc --version
|
||||||
- cat tests/tools/taproot_signer/Cargo.lock
|
- cat tests/tools/taproot_signer/Cargo.lock
|
||||||
lianad_build_script: cd liana && cargo build --release && cd ../tests/tools/taproot_signer && cargo build --release
|
lianad_build_script: cd lianad && cargo build --release && cd ../tests/tools/taproot_signer && cargo build --release
|
||||||
|
|
||||||
deps_script: apt update && apt install -y python3 python3-pip
|
deps_script: apt update && apt install -y python3 python3-pip
|
||||||
|
|
||||||
|
|||||||
27
Cargo.lock
generated
27
Cargo.lock
generated
@ -2804,21 +2804,13 @@ dependencies = [
|
|||||||
name = "liana"
|
name = "liana"
|
||||||
version = "8.0.0"
|
version = "8.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
|
||||||
"bdk_coin_select",
|
"bdk_coin_select",
|
||||||
"bdk_electrum",
|
|
||||||
"bip39",
|
"bip39",
|
||||||
"dirs 5.0.1",
|
|
||||||
"fern",
|
|
||||||
"getrandom",
|
"getrandom",
|
||||||
"jsonrpc 0.17.0",
|
|
||||||
"log",
|
"log",
|
||||||
"miniscript",
|
"miniscript",
|
||||||
"rdrand",
|
"rdrand",
|
||||||
"rusqlite",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
|
||||||
"toml",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2850,6 +2842,7 @@ dependencies = [
|
|||||||
"jsonrpc 0.12.1",
|
"jsonrpc 0.12.1",
|
||||||
"liana",
|
"liana",
|
||||||
"liana-ui",
|
"liana-ui",
|
||||||
|
"lianad",
|
||||||
"log",
|
"log",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rust-ini",
|
"rust-ini",
|
||||||
@ -2872,6 +2865,24 @@ dependencies = [
|
|||||||
"iced",
|
"iced",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lianad"
|
||||||
|
version = "8.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"backtrace",
|
||||||
|
"bdk_electrum",
|
||||||
|
"dirs 5.0.1",
|
||||||
|
"fern",
|
||||||
|
"jsonrpc 0.17.0",
|
||||||
|
"liana",
|
||||||
|
"log",
|
||||||
|
"miniscript",
|
||||||
|
"rusqlite",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.162"
|
version = "0.2.162"
|
||||||
|
|||||||
@ -3,10 +3,11 @@ resolver = "2"
|
|||||||
members = [
|
members = [
|
||||||
"fuzz",
|
"fuzz",
|
||||||
"liana",
|
"liana",
|
||||||
|
"lianad",
|
||||||
"liana-gui",
|
"liana-gui",
|
||||||
"liana-ui",
|
"liana-ui",
|
||||||
]
|
]
|
||||||
default-members = ["liana", "liana-gui", "liana-ui"]
|
default-members = ["liana", "lianad", "liana-gui", "liana-ui"]
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
iced_style = { git = "https://github.com/edouardparis/iced", branch = "patch-0.12.3"}
|
iced_style = { git = "https://github.com/edouardparis/iced", branch = "patch-0.12.3"}
|
||||||
|
|||||||
@ -16,7 +16,8 @@ path = "src/main.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
async-hwi = { version = "0.0.24" }
|
async-hwi = { version = "0.0.24" }
|
||||||
liana = { path = "../liana", default-features = false, features = ["nonblocking_shutdown"] }
|
liana = { path = "../liana" }
|
||||||
|
lianad = { path = "../lianad", default-features = false, features = ["nonblocking_shutdown"] }
|
||||||
liana-ui = { path = "../liana-ui" }
|
liana-ui = { path = "../liana-ui" }
|
||||||
backtrace = "0.3"
|
backtrace = "0.3"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
use std::convert::From;
|
use std::convert::From;
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
|
|
||||||
use liana::{config::ConfigError, descriptors::LianaDescError, spend::SpendCreationError};
|
use liana::{descriptors::LianaDescError, spend::SpendCreationError};
|
||||||
|
use lianad::config::ConfigError;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{settings::SettingsError, wallet::WalletError},
|
app::{settings::SettingsError, wallet::WalletError},
|
||||||
|
|||||||
@ -1,14 +1,12 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use liana::{
|
use liana::miniscript::bitcoin::{
|
||||||
config::Config as DaemonConfig,
|
bip32::{ChildNumber, Fingerprint},
|
||||||
miniscript::bitcoin::{
|
psbt::Psbt,
|
||||||
bip32::{ChildNumber, Fingerprint},
|
Address, Txid,
|
||||||
psbt::Psbt,
|
|
||||||
Address, Txid,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
use lianad::config::Config as DaemonConfig;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{cache::Cache, error::Error, view, wallet::Wallet},
|
app::{cache::Cache, error::Error, view, wallet::Wallet},
|
||||||
|
|||||||
@ -19,11 +19,12 @@ use iced::{clipboard, time, Command, Subscription};
|
|||||||
use tokio::runtime::Handle;
|
use tokio::runtime::Handle;
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
pub use liana::{commands::CoinStatus, config::Config as DaemonConfig, miniscript::bitcoin};
|
pub use liana::miniscript::bitcoin;
|
||||||
use liana_ui::{
|
use liana_ui::{
|
||||||
component::network_banner,
|
component::network_banner,
|
||||||
widget::{Column, Element},
|
widget::{Column, Element},
|
||||||
};
|
};
|
||||||
|
pub use lianad::{commands::CoinStatus, config::Config as DaemonConfig};
|
||||||
|
|
||||||
pub use config::Config;
|
pub use config::Config;
|
||||||
pub use message::Message;
|
pub use message::Message;
|
||||||
|
|||||||
@ -4,8 +4,8 @@ use std::{cmp::Ordering, collections::HashSet};
|
|||||||
|
|
||||||
use iced::Command;
|
use iced::Command;
|
||||||
|
|
||||||
use liana::commands::CoinStatus;
|
|
||||||
use liana_ui::widget::Element;
|
use liana_ui::widget::Element;
|
||||||
|
use lianad::commands::CoinStatus;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{
|
app::{
|
||||||
|
|||||||
@ -13,11 +13,9 @@ use std::sync::Arc;
|
|||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use iced::{Command, Subscription};
|
use iced::{Command, Subscription};
|
||||||
use liana::{
|
use liana::miniscript::bitcoin::{Amount, OutPoint};
|
||||||
commands::CoinStatus,
|
|
||||||
miniscript::bitcoin::{Amount, OutPoint},
|
|
||||||
};
|
|
||||||
use liana_ui::widget::*;
|
use liana_ui::widget::*;
|
||||||
|
use lianad::commands::CoinStatus;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
cache::Cache,
|
cache::Cache,
|
||||||
|
|||||||
@ -7,10 +7,10 @@ use iced::Subscription;
|
|||||||
|
|
||||||
use iced::Command;
|
use iced::Command;
|
||||||
use liana::{
|
use liana::{
|
||||||
commands::CoinStatus,
|
|
||||||
descriptors::LianaPolicy,
|
descriptors::LianaPolicy,
|
||||||
miniscript::bitcoin::{bip32::Fingerprint, psbt::Psbt, Network, Txid},
|
miniscript::bitcoin::{bip32::Fingerprint, psbt::Psbt, Network, Txid},
|
||||||
};
|
};
|
||||||
|
use lianad::commands::CoinStatus;
|
||||||
|
|
||||||
use liana_ui::component::toast;
|
use liana_ui::component::toast;
|
||||||
use liana_ui::{
|
use liana_ui::{
|
||||||
|
|||||||
@ -4,14 +4,12 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use iced::Command;
|
use iced::Command;
|
||||||
|
|
||||||
use liana::{
|
use liana::miniscript::bitcoin::{
|
||||||
commands::CoinStatus,
|
bip32::{DerivationPath, Fingerprint},
|
||||||
miniscript::bitcoin::{
|
secp256k1,
|
||||||
bip32::{DerivationPath, Fingerprint},
|
|
||||||
secp256k1,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use liana_ui::{component::form, widget::Element};
|
use liana_ui::{component::form, widget::Element};
|
||||||
|
use lianad::commands::CoinStatus;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{
|
app::{
|
||||||
|
|||||||
@ -8,11 +8,9 @@ use chrono::{NaiveDate, Utc};
|
|||||||
use iced::Command;
|
use iced::Command;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use liana::{
|
use liana::miniscript::bitcoin::Network;
|
||||||
config::{
|
use lianad::config::{
|
||||||
BitcoinBackend, BitcoinConfig, BitcoindConfig, BitcoindRpcAuth, Config, ElectrumConfig,
|
BitcoinBackend, BitcoinConfig, BitcoindConfig, BitcoindRpcAuth, Config, ElectrumConfig,
|
||||||
},
|
|
||||||
miniscript::bitcoin::Network,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use liana_ui::{component::form, widget::Element};
|
use liana_ui::{component::form, widget::Element};
|
||||||
@ -353,7 +351,7 @@ impl BitcoindSettings {
|
|||||||
if let (true, Some(rpc_auth)) = (self.addr.valid, rpc_auth) {
|
if let (true, Some(rpc_auth)) = (self.addr.valid, rpc_auth) {
|
||||||
let mut daemon_config = daemon.config().cloned().unwrap();
|
let mut daemon_config = daemon.config().cloned().unwrap();
|
||||||
daemon_config.bitcoin_backend =
|
daemon_config.bitcoin_backend =
|
||||||
Some(liana::config::BitcoinBackend::Bitcoind(BitcoindConfig {
|
Some(lianad::config::BitcoinBackend::Bitcoind(BitcoindConfig {
|
||||||
rpc_auth,
|
rpc_auth,
|
||||||
addr: new_addr.unwrap(),
|
addr: new_addr.unwrap(),
|
||||||
}));
|
}));
|
||||||
@ -461,7 +459,7 @@ impl ElectrumSettings {
|
|||||||
if self.addr.valid {
|
if self.addr.valid {
|
||||||
let mut daemon_config = daemon.config().cloned().unwrap();
|
let mut daemon_config = daemon.config().cloned().unwrap();
|
||||||
daemon_config.bitcoin_backend =
|
daemon_config.bitcoin_backend =
|
||||||
Some(liana::config::BitcoinBackend::Electrum(ElectrumConfig {
|
Some(lianad::config::BitcoinBackend::Electrum(ElectrumConfig {
|
||||||
addr: self.addr.value.clone(),
|
addr: self.addr.value.clone(),
|
||||||
}));
|
}));
|
||||||
self.processing = true;
|
self.processing = true;
|
||||||
|
|||||||
@ -5,11 +5,9 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use iced::Command;
|
use iced::Command;
|
||||||
|
|
||||||
use liana::{
|
use liana::miniscript::bitcoin::{Network, OutPoint};
|
||||||
commands::CoinStatus,
|
|
||||||
miniscript::bitcoin::{Network, OutPoint},
|
|
||||||
};
|
|
||||||
use liana_ui::widget::Element;
|
use liana_ui::widget::Element;
|
||||||
|
use lianad::commands::CoinStatus;
|
||||||
|
|
||||||
use super::{redirect, State};
|
use super::{redirect, State};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|||||||
@ -8,13 +8,13 @@ use std::{
|
|||||||
|
|
||||||
use iced::{Command, Subscription};
|
use iced::{Command, Subscription};
|
||||||
use liana::{
|
use liana::{
|
||||||
commands::ListCoinsEntry,
|
|
||||||
descriptors::LianaDescriptor,
|
descriptors::LianaDescriptor,
|
||||||
miniscript::bitcoin::{
|
miniscript::bitcoin::{
|
||||||
address, psbt::Psbt, secp256k1, Address, Amount, Denomination, Network, OutPoint,
|
address, psbt::Psbt, secp256k1, Address, Amount, Denomination, Network, OutPoint,
|
||||||
},
|
},
|
||||||
spend::{SpendCreationError, MAX_FEERATE},
|
spend::{SpendCreationError, MAX_FEERATE},
|
||||||
};
|
};
|
||||||
|
use lianad::commands::ListCoinsEntry;
|
||||||
|
|
||||||
use liana_ui::{component::form, widget::Element};
|
use liana_ui::{component::form, widget::Element};
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,6 @@ use std::{
|
|||||||
|
|
||||||
use iced::Command;
|
use iced::Command;
|
||||||
use liana::{
|
use liana::{
|
||||||
commands::CoinStatus,
|
|
||||||
miniscript::bitcoin::{OutPoint, Txid},
|
miniscript::bitcoin::{OutPoint, Txid},
|
||||||
spend::{SpendCreationError, MAX_FEERATE},
|
spend::{SpendCreationError, MAX_FEERATE},
|
||||||
};
|
};
|
||||||
@ -15,6 +14,7 @@ use liana_ui::{
|
|||||||
component::{form, modal::Modal},
|
component::{form, modal::Modal},
|
||||||
widget::*,
|
widget::*,
|
||||||
};
|
};
|
||||||
|
use lianad::commands::CoinStatus;
|
||||||
|
|
||||||
pub const HISTORY_EVENT_PAGE_SIZE: u64 = 20;
|
pub const HISTORY_EVENT_PAGE_SIZE: u64 = 20;
|
||||||
|
|
||||||
|
|||||||
@ -8,10 +8,10 @@ use iced::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use liana::{
|
use liana::{
|
||||||
config::BitcoindRpcAuth,
|
|
||||||
descriptors::{LianaDescriptor, LianaPolicy},
|
descriptors::{LianaDescriptor, LianaPolicy},
|
||||||
miniscript::bitcoin::{bip32::Fingerprint, Network},
|
miniscript::bitcoin::{bip32::Fingerprint, Network},
|
||||||
};
|
};
|
||||||
|
use lianad::config::BitcoindRpcAuth;
|
||||||
|
|
||||||
use super::{dashboard, message::*};
|
use super::{dashboard, message::*};
|
||||||
|
|
||||||
@ -452,7 +452,7 @@ pub fn bitcoind_edit<'a>(
|
|||||||
pub fn bitcoind<'a>(
|
pub fn bitcoind<'a>(
|
||||||
is_configured_node_type: bool,
|
is_configured_node_type: bool,
|
||||||
network: Network,
|
network: Network,
|
||||||
config: &liana::config::BitcoindConfig,
|
config: &lianad::config::BitcoindConfig,
|
||||||
blockheight: i32,
|
blockheight: i32,
|
||||||
is_running: Option<bool>,
|
is_running: Option<bool>,
|
||||||
can_edit: bool,
|
can_edit: bool,
|
||||||
@ -638,7 +638,7 @@ pub fn electrum_edit<'a>(
|
|||||||
pub fn electrum<'a>(
|
pub fn electrum<'a>(
|
||||||
is_configured_node_type: bool,
|
is_configured_node_type: bool,
|
||||||
network: Network,
|
network: Network,
|
||||||
config: &liana::config::ElectrumConfig,
|
config: &lianad::config::ElectrumConfig,
|
||||||
blockheight: i32,
|
blockheight: i32,
|
||||||
is_running: Option<bool>,
|
is_running: Option<bool>,
|
||||||
can_edit: bool,
|
can_edit: bool,
|
||||||
|
|||||||
@ -4,7 +4,6 @@ use std::iter::FromIterator;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use liana::commands::{CoinStatus, CreateRecoveryResult};
|
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
@ -13,10 +12,10 @@ use tracing::{error, info};
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod jsonrpc;
|
pub mod jsonrpc;
|
||||||
|
|
||||||
use liana::{
|
use liana::miniscript::bitcoin::{address, psbt::Psbt, Address, Network, OutPoint, Txid};
|
||||||
commands::LabelItem,
|
use lianad::{
|
||||||
|
commands::{CoinStatus, CreateRecoveryResult, LabelItem},
|
||||||
config::Config,
|
config::Config,
|
||||||
miniscript::bitcoin::{address, psbt::Psbt, Address, Network, OutPoint, Txid},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{model::*, Daemon, DaemonBackend, DaemonError};
|
use super::{model::*, Daemon, DaemonBackend, DaemonError};
|
||||||
|
|||||||
@ -4,10 +4,10 @@ use tokio::sync::Mutex;
|
|||||||
|
|
||||||
use super::{model::*, node, Daemon, DaemonBackend, DaemonError};
|
use super::{model::*, node, Daemon, DaemonBackend, DaemonError};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use liana::{
|
use liana::miniscript::bitcoin::{address, psbt::Psbt, Address, Network, OutPoint, Txid};
|
||||||
|
use lianad::{
|
||||||
commands::{CoinStatus, LabelItem},
|
commands::{CoinStatus, LabelItem},
|
||||||
config::Config,
|
config::Config,
|
||||||
miniscript::bitcoin::{address, psbt::Psbt, Address, Network, OutPoint, Txid},
|
|
||||||
DaemonControl, DaemonHandle,
|
DaemonControl, DaemonHandle,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -11,12 +11,12 @@ use std::path::Path;
|
|||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
use liana::{
|
use liana::miniscript::bitcoin::{
|
||||||
|
address, bip32::Fingerprint, psbt::Psbt, secp256k1, Address, Network, OutPoint, Txid,
|
||||||
|
};
|
||||||
|
use lianad::{
|
||||||
commands::{CoinStatus, LabelItem, TransactionInfo},
|
commands::{CoinStatus, LabelItem, TransactionInfo},
|
||||||
config::Config,
|
config::Config,
|
||||||
miniscript::bitcoin::{
|
|
||||||
address, bip32::Fingerprint, psbt::Psbt, secp256k1, Address, Network, OutPoint, Txid,
|
|
||||||
},
|
|
||||||
StartupError,
|
StartupError,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -3,11 +3,6 @@ use std::collections::{HashMap, HashSet};
|
|||||||
|
|
||||||
use liana::descriptors::LianaDescriptor;
|
use liana::descriptors::LianaDescriptor;
|
||||||
pub use liana::{
|
pub use liana::{
|
||||||
commands::{
|
|
||||||
CreateSpendResult, GetAddressResult, GetInfoResult, GetLabelsResult, LabelItem,
|
|
||||||
ListCoinsEntry, ListCoinsResult, ListSpendEntry, ListSpendResult, ListTransactionsResult,
|
|
||||||
TransactionInfo,
|
|
||||||
},
|
|
||||||
descriptors::{LianaPolicy, PartialSpendInfo, PathSpendInfo},
|
descriptors::{LianaPolicy, PartialSpendInfo, PathSpendInfo},
|
||||||
miniscript::bitcoin::{
|
miniscript::bitcoin::{
|
||||||
bip32::{DerivationPath, Fingerprint},
|
bip32::{DerivationPath, Fingerprint},
|
||||||
@ -15,6 +10,10 @@ pub use liana::{
|
|||||||
secp256k1, Address, Amount, Network, OutPoint, Transaction, Txid,
|
secp256k1, Address, Amount, Network, OutPoint, Transaction, Txid,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
pub use lianad::commands::{
|
||||||
|
CreateSpendResult, GetAddressResult, GetInfoResult, GetLabelsResult, LabelItem, ListCoinsEntry,
|
||||||
|
ListCoinsResult, ListSpendEntry, ListSpendResult, ListTransactionsResult, TransactionInfo,
|
||||||
|
};
|
||||||
|
|
||||||
pub type Coin = ListCoinsEntry;
|
pub type Coin = ListCoinsEntry;
|
||||||
|
|
||||||
|
|||||||
@ -9,11 +9,8 @@ use crate::{
|
|||||||
signer::Signer,
|
signer::Signer,
|
||||||
};
|
};
|
||||||
use async_hwi::DeviceKind;
|
use async_hwi::DeviceKind;
|
||||||
use liana::{
|
use liana::{descriptors::LianaDescriptor, miniscript::bitcoin};
|
||||||
config::{BitcoinBackend, BitcoinConfig},
|
use lianad::config::{BitcoinBackend, BitcoinConfig};
|
||||||
descriptors::LianaDescriptor,
|
|
||||||
miniscript::bitcoin,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum RemoteBackend {
|
pub enum RemoteBackend {
|
||||||
|
|||||||
@ -5,14 +5,12 @@ mod step;
|
|||||||
mod view;
|
mod view;
|
||||||
|
|
||||||
use iced::{clipboard, Command, Subscription};
|
use iced::{clipboard, Command, Subscription};
|
||||||
use liana::{
|
use liana::miniscript::bitcoin::{self, Network};
|
||||||
config::Config,
|
|
||||||
miniscript::bitcoin::{self, Network},
|
|
||||||
};
|
|
||||||
use liana_ui::{
|
use liana_ui::{
|
||||||
component::network_banner,
|
component::network_banner,
|
||||||
widget::{Column, Element},
|
widget::{Column, Element},
|
||||||
};
|
};
|
||||||
|
use lianad::config::Config;
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
use context::{Context, RemoteBackend};
|
use context::{Context, RemoteBackend};
|
||||||
@ -332,9 +330,9 @@ impl Installer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn daemon_check(cfg: liana::config::Config) -> Result<(), Error> {
|
pub fn daemon_check(cfg: lianad::config::Config) -> Result<(), Error> {
|
||||||
// Start Daemon to check correctness of installation
|
// Start Daemon to check correctness of installation
|
||||||
match liana::DaemonHandle::start_default(cfg, false) {
|
match lianad::DaemonHandle::start_default(cfg, false) {
|
||||||
Ok(daemon) => daemon
|
Ok(daemon) => daemon
|
||||||
.stop()
|
.stop()
|
||||||
.map_err(|e| Error::Unexpected(format!("Failed to stop Liana daemon: {}", e))),
|
.map_err(|e| Error::Unexpected(format!("Failed to stop Liana daemon: {}", e))),
|
||||||
@ -349,7 +347,7 @@ pub async fn install_local_wallet(
|
|||||||
ctx: Context,
|
ctx: Context,
|
||||||
signer: Arc<Mutex<Signer>>,
|
signer: Arc<Mutex<Signer>>,
|
||||||
) -> Result<PathBuf, Error> {
|
) -> Result<PathBuf, Error> {
|
||||||
let mut cfg: liana::config::Config = extract_daemon_config(&ctx);
|
let mut cfg: lianad::config::Config = extract_daemon_config(&ctx);
|
||||||
let data_dir = cfg.data_dir.unwrap();
|
let data_dir = cfg.data_dir.unwrap();
|
||||||
|
|
||||||
let data_dir = data_dir
|
let data_dir = data_dir
|
||||||
|
|||||||
@ -8,10 +8,8 @@ use bitcoin_hashes::{sha256, Hash};
|
|||||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||||
use flate2::read::GzDecoder;
|
use flate2::read::GzDecoder;
|
||||||
use iced::{Command, Subscription};
|
use iced::{Command, Subscription};
|
||||||
use liana::{
|
use liana::miniscript::bitcoin::Network;
|
||||||
config::{BitcoinBackend, BitcoindConfig, BitcoindRpcAuth},
|
use lianad::config::{BitcoinBackend, BitcoindConfig, BitcoindRpcAuth};
|
||||||
miniscript::bitcoin::Network,
|
|
||||||
};
|
|
||||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||||
use tar::Archive;
|
use tar::Archive;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
@ -473,7 +471,7 @@ impl DefineBitcoind {
|
|||||||
}
|
}
|
||||||
(Some(rpc_auth), Ok(addr)) => {
|
(Some(rpc_auth), Ok(addr)) => {
|
||||||
ctx.bitcoin_backend =
|
ctx.bitcoin_backend =
|
||||||
Some(liana::config::BitcoinBackend::Bitcoind(BitcoindConfig {
|
Some(lianad::config::BitcoinBackend::Bitcoind(BitcoindConfig {
|
||||||
rpc_auth,
|
rpc_auth,
|
||||||
addr,
|
addr,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
use iced::Command;
|
use iced::Command;
|
||||||
use liana::{
|
use liana_ui::{component::form, widget::*};
|
||||||
|
use lianad::{
|
||||||
config::ElectrumConfig,
|
config::ElectrumConfig,
|
||||||
electrum_client::{self, ElectrumApi},
|
electrum_client::{self, ElectrumApi},
|
||||||
};
|
};
|
||||||
use liana_ui::{component::form, widget::*};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
installer::{
|
installer::{
|
||||||
@ -45,7 +45,7 @@ impl DefineElectrum {
|
|||||||
|
|
||||||
pub fn apply(&mut self, ctx: &mut Context) -> bool {
|
pub fn apply(&mut self, ctx: &mut Context) -> bool {
|
||||||
if self.can_try_ping() {
|
if self.can_try_ping() {
|
||||||
ctx.bitcoin_backend = Some(liana::config::BitcoinBackend::Electrum(ElectrumConfig {
|
ctx.bitcoin_backend = Some(lianad::config::BitcoinBackend::Electrum(ElectrumConfig {
|
||||||
addr: self.address.value.clone(),
|
addr: self.address.value.clone(),
|
||||||
}));
|
}));
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@ -6,13 +6,14 @@ use iced::{
|
|||||||
Alignment, Command, Length, Subscription,
|
Alignment, Command, Length, Subscription,
|
||||||
};
|
};
|
||||||
|
|
||||||
use liana::{config::ConfigError, miniscript::bitcoin::Network};
|
use liana::miniscript::bitcoin::Network;
|
||||||
use liana_ui::{
|
use liana_ui::{
|
||||||
color,
|
color,
|
||||||
component::{button, card, modal::Modal, network_banner, notification, text::*},
|
component::{button, card, modal::Modal, network_banner, notification, text::*},
|
||||||
icon, image, theme,
|
icon, image, theme,
|
||||||
widget::*,
|
widget::*,
|
||||||
};
|
};
|
||||||
|
use lianad::config::ConfigError;
|
||||||
|
|
||||||
use crate::{app, installer::UserFlow};
|
use crate::{app, installer::UserFlow};
|
||||||
|
|
||||||
@ -466,7 +467,7 @@ async fn check_network_datadir(path: PathBuf, network: Network) -> Result<State,
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(daemon_config_path) = cfg.daemon_config_path {
|
if let Some(daemon_config_path) = cfg.daemon_config_path {
|
||||||
liana::config::Config::from_file(Some(daemon_config_path.clone())).map_err(|e| match e {
|
lianad::config::Config::from_file(Some(daemon_config_path.clone())).map_err(|e| match e {
|
||||||
ConfigError::FileNotFound
|
ConfigError::FileNotFound
|
||||||
| ConfigError::DatadirNotFound => {
|
| ConfigError::DatadirNotFound => {
|
||||||
format!(
|
format!(
|
||||||
|
|||||||
@ -9,11 +9,13 @@ use std::{
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use liana::{
|
use liana::{
|
||||||
commands::{CoinStatus, GetInfoDescriptors, LCSpendInfo, LabelItem},
|
|
||||||
config::Config,
|
|
||||||
descriptors::LianaDescriptor,
|
descriptors::LianaDescriptor,
|
||||||
miniscript::bitcoin::{address, psbt::Psbt, Address, Network, OutPoint, Txid},
|
miniscript::bitcoin::{address, psbt::Psbt, Address, Network, OutPoint, Txid},
|
||||||
};
|
};
|
||||||
|
use lianad::{
|
||||||
|
commands::{CoinStatus, GetInfoDescriptors, LCSpendInfo, LabelItem},
|
||||||
|
config::Config,
|
||||||
|
};
|
||||||
use reqwest::{Error, IntoUrl, Method, RequestBuilder, Response};
|
use reqwest::{Error, IntoUrl, Method, RequestBuilder, Response};
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@ pub mod node;
|
|||||||
pub mod signer;
|
pub mod signer;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
use liana::Version;
|
use lianad::Version;
|
||||||
|
|
||||||
pub const VERSION: Version = Version {
|
pub const VERSION: Version = Version {
|
||||||
major: 8,
|
major: 8,
|
||||||
|
|||||||
@ -9,18 +9,18 @@ use iced::{Alignment, Command, Length, Subscription};
|
|||||||
use tokio::runtime::Handle;
|
use tokio::runtime::Handle;
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
use liana::{
|
use liana::miniscript::bitcoin;
|
||||||
commands::CoinStatus,
|
|
||||||
config::{BitcoinBackend, Config, ConfigError},
|
|
||||||
miniscript::bitcoin,
|
|
||||||
StartupError,
|
|
||||||
};
|
|
||||||
use liana_ui::{
|
use liana_ui::{
|
||||||
color,
|
color,
|
||||||
component::{button, notification, text::*},
|
component::{button, notification, text::*},
|
||||||
icon,
|
icon,
|
||||||
widget::*,
|
widget::*,
|
||||||
};
|
};
|
||||||
|
use lianad::{
|
||||||
|
commands::CoinStatus,
|
||||||
|
config::{BitcoinBackend, Config, ConfigError},
|
||||||
|
StartupError,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{
|
app::{
|
||||||
@ -533,7 +533,7 @@ pub async fn start_bitcoind_and_daemon(
|
|||||||
if start_internal_bitcoind {
|
if start_internal_bitcoind {
|
||||||
if let Some(BitcoinBackend::Bitcoind(bitcoind_config)) = &config.bitcoin_backend {
|
if let Some(BitcoinBackend::Bitcoind(bitcoind_config)) = &config.bitcoin_backend {
|
||||||
// Check if bitcoind is already running before trying to start it.
|
// Check if bitcoind is already running before trying to start it.
|
||||||
if liana::BitcoinD::new(bitcoind_config, "internal_bitcoind_start".to_string()).is_ok()
|
if lianad::BitcoinD::new(bitcoind_config, "internal_bitcoind_start".to_string()).is_ok()
|
||||||
{
|
{
|
||||||
info!("Internal bitcoind is already running");
|
info!("Internal bitcoind is already running");
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -17,8 +17,9 @@ use tracing_subscriber::filter::LevelFilter;
|
|||||||
extern crate serde;
|
extern crate serde;
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
|
|
||||||
use liana::{config::Config as DaemonConfig, miniscript::bitcoin};
|
use liana::miniscript::bitcoin;
|
||||||
use liana_ui::{component::text, font, image, theme, widget::Element};
|
use liana_ui::{component::text, font, image, theme, widget::Element};
|
||||||
|
use lianad::config::Config as DaemonConfig;
|
||||||
|
|
||||||
use liana_gui::{
|
use liana_gui::{
|
||||||
app::{self, cache::Cache, config::default_datadir, wallet::Wallet, App},
|
app::{self, cache::Cache, config::default_datadir, wallet::Wallet, App},
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use bitcoin_hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine};
|
use bitcoin_hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine};
|
||||||
use liana::{
|
use liana::{
|
||||||
config::BitcoindConfig,
|
|
||||||
miniscript::bitcoin::{self, Network},
|
miniscript::bitcoin::{self, Network},
|
||||||
random::{random_bytes, RandomnessError},
|
random::{random_bytes, RandomnessError},
|
||||||
};
|
};
|
||||||
use liana_ui::component::form;
|
use liana_ui::component::form;
|
||||||
|
use lianad::config::BitcoindConfig;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@ -462,7 +462,7 @@ impl Bitcoind {
|
|||||||
return Err(StartInternalBitcoindError::ProcessExited(status));
|
return Err(StartInternalBitcoindError::ProcessExited(status));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match liana::BitcoinD::new(&config, "internal_bitcoind_start".to_string()) {
|
match lianad::BitcoinD::new(&config, "internal_bitcoind_start".to_string()) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
log::info!("Bitcoind seems to have successfully started.");
|
log::info!("Bitcoind seems to have successfully started.");
|
||||||
return Ok(Self {
|
return Ok(Self {
|
||||||
@ -470,7 +470,7 @@ impl Bitcoind {
|
|||||||
_process: Arc::new(process),
|
_process: Arc::new(process),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(liana::BitcoindError::CookieFile(_)) => {
|
Err(lianad::BitcoindError::CookieFile(_)) => {
|
||||||
// This is only raised if we're using cookie authentication.
|
// This is only raised if we're using cookie authentication.
|
||||||
// Assume cookie file has not been created yet and try again.
|
// Assume cookie file has not been created yet and try again.
|
||||||
}
|
}
|
||||||
@ -497,7 +497,7 @@ impl Bitcoind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn stop_bitcoind(config: &BitcoindConfig) -> bool {
|
pub fn stop_bitcoind(config: &BitcoindConfig) -> bool {
|
||||||
match liana::BitcoinD::new(config, "internal_bitcoind_stop".to_string()) {
|
match lianad::BitcoinD::new(config, "internal_bitcoind_stop".to_string()) {
|
||||||
Ok(bitcoind) => {
|
Ok(bitcoind) => {
|
||||||
info!("Stopping internal bitcoind...");
|
info!("Stopping internal bitcoind...");
|
||||||
bitcoind.stop();
|
bitcoind.stop();
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use liana::config::BitcoinBackend;
|
use lianad::config::BitcoinBackend;
|
||||||
|
|
||||||
pub mod bitcoind;
|
pub mod bitcoind;
|
||||||
pub mod electrum;
|
pub mod electrum;
|
||||||
|
|||||||
@ -6,19 +6,7 @@ edition = "2018"
|
|||||||
repository = "https://github.com/wizardsardine/liana"
|
repository = "https://github.com/wizardsardine/liana"
|
||||||
license-file = "LICENCE"
|
license-file = "LICENCE"
|
||||||
keywords = ["bitcoin", "wallet", "miniscript", "inheritance", "recovery"]
|
keywords = ["bitcoin", "wallet", "miniscript", "inheritance", "recovery"]
|
||||||
description = "Liana wallet daemon"
|
description = "Liana development kit"
|
||||||
exclude = [".github/", ".cirrus.yml", "tests/", "test_data/", "contrib/", "pyproject.toml"]
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "lianad"
|
|
||||||
path = "src/bin/daemon.rs"
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "liana-cli"
|
|
||||||
path = "src/bin/cli.rs"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
nonblocking_shutdown = []
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# For managing transactions (it re-exports the bitcoin crate)
|
# For managing transactions (it re-exports the bitcoin crate)
|
||||||
@ -26,34 +14,11 @@ miniscript = { version = "11.0", features = ["serde", "compiler", "base64"] }
|
|||||||
|
|
||||||
# Coin selection algorithms for spend transaction creation.
|
# Coin selection algorithms for spend transaction creation.
|
||||||
bdk_coin_select = "0.3"
|
bdk_coin_select = "0.3"
|
||||||
|
|
||||||
# For Electrum backend. This is the latest version with the same bitcoin version as
|
|
||||||
# the miniscript dependency.
|
|
||||||
bdk_electrum = { version = "0.14" }
|
|
||||||
|
|
||||||
# Don't reinvent the wheel
|
|
||||||
dirs = "5.0"
|
|
||||||
|
|
||||||
# We use TOML for the config, and JSON for RPC
|
# We use TOML for the config, and JSON for RPC
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
toml = "0.5"
|
|
||||||
serde_json = { version = "1.0", features = ["raw_value"] }
|
|
||||||
|
|
||||||
# Logging stuff
|
# Logging stuff
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
fern = "0.6"
|
|
||||||
|
|
||||||
# In order to have a backtrace on panic, because the
|
|
||||||
# stdlib does not have a programmatic interface yet
|
|
||||||
# to work with our custom panic hook.
|
|
||||||
backtrace = "0.3"
|
|
||||||
|
|
||||||
# Pinned to this version because they keep breaking their MSRV in point releases...
|
|
||||||
# FIXME: this is unfortunate, we don't receive the updates (sometimes critical) from SQLite.
|
|
||||||
rusqlite = { version = "0.30", features = ["bundled", "unlock_notify"] }
|
|
||||||
|
|
||||||
# To talk to bitcoind
|
|
||||||
jsonrpc = { version = "0.17", features = ["minreq_http"], default-features = false }
|
|
||||||
|
|
||||||
# Used for generating mnemonics
|
# Used for generating mnemonics
|
||||||
getrandom = "0.2"
|
getrandom = "0.2"
|
||||||
|
|||||||
867
liana/src/lib.rs
867
liana/src/lib.rs
@ -1,874 +1,7 @@
|
|||||||
mod bitcoin;
|
|
||||||
pub mod commands;
|
|
||||||
pub mod config;
|
|
||||||
mod database;
|
|
||||||
pub mod descriptors;
|
pub mod descriptors;
|
||||||
mod jsonrpc;
|
|
||||||
pub mod random;
|
pub mod random;
|
||||||
pub mod signer;
|
pub mod signer;
|
||||||
pub mod spend;
|
pub mod spend;
|
||||||
#[cfg(test)]
|
|
||||||
mod testutils;
|
|
||||||
|
|
||||||
pub use bdk_electrum::electrum_client;
|
|
||||||
pub use bip39;
|
pub use bip39;
|
||||||
use bitcoin::electrum;
|
|
||||||
pub use miniscript;
|
pub use miniscript;
|
||||||
|
|
||||||
pub use crate::bitcoin::{
|
|
||||||
d::{BitcoinD, BitcoindError, WalletError},
|
|
||||||
electrum::{Electrum, ElectrumError},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::jsonrpc::server;
|
|
||||||
use crate::{
|
|
||||||
bitcoin::{poller, BitcoinInterface},
|
|
||||||
config::Config,
|
|
||||||
database::{
|
|
||||||
sqlite::{FreshDbOptions, SqliteDb, SqliteDbError, MAX_DB_VERSION_NO_TX_DB},
|
|
||||||
DatabaseInterface,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
use std::{
|
|
||||||
collections, error, fmt, fs, io, path,
|
|
||||||
sync::{self, mpsc},
|
|
||||||
thread,
|
|
||||||
};
|
|
||||||
|
|
||||||
use miniscript::bitcoin::{constants::ChainHash, hashes::Hash, secp256k1, BlockHash};
|
|
||||||
|
|
||||||
#[cfg(not(test))]
|
|
||||||
use std::panic;
|
|
||||||
// A panic in any thread should stop the main thread, and print the panic.
|
|
||||||
#[cfg(not(test))]
|
|
||||||
fn setup_panic_hook() {
|
|
||||||
panic::set_hook(Box::new(move |panic_info| {
|
|
||||||
let file = panic_info
|
|
||||||
.location()
|
|
||||||
.map(|l| l.file())
|
|
||||||
.unwrap_or_else(|| "'unknown'");
|
|
||||||
let line = panic_info
|
|
||||||
.location()
|
|
||||||
.map(|l| l.line().to_string())
|
|
||||||
.unwrap_or_else(|| "'unknown'".to_string());
|
|
||||||
|
|
||||||
let bt = backtrace::Backtrace::new();
|
|
||||||
let info = panic_info
|
|
||||||
.payload()
|
|
||||||
.downcast_ref::<&str>()
|
|
||||||
.map(|s| s.to_string())
|
|
||||||
.or_else(|| panic_info.payload().downcast_ref::<String>().cloned());
|
|
||||||
log::error!(
|
|
||||||
"panic occurred at line {} of file {}: {:?}\n{:?}",
|
|
||||||
line,
|
|
||||||
file,
|
|
||||||
info,
|
|
||||||
bt
|
|
||||||
);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Version {
|
|
||||||
pub major: u32,
|
|
||||||
pub minor: u32,
|
|
||||||
pub patch: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Version {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}.{}.{}-dev", self.major, self.minor, self.patch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const VERSION: Version = Version {
|
|
||||||
major: 8,
|
|
||||||
minor: 0,
|
|
||||||
patch: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum StartupError {
|
|
||||||
Io(io::Error),
|
|
||||||
DefaultDataDirNotFound,
|
|
||||||
DatadirCreation(path::PathBuf, io::Error),
|
|
||||||
MissingBitcoindConfig,
|
|
||||||
MissingElectrumConfig,
|
|
||||||
MissingBitcoinBackendConfig,
|
|
||||||
DbMigrateBitcoinTxs(&'static str),
|
|
||||||
Database(SqliteDbError),
|
|
||||||
Bitcoind(BitcoindError),
|
|
||||||
Electrum(ElectrumError),
|
|
||||||
#[cfg(windows)]
|
|
||||||
NoWatchonlyInDatadir,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for StartupError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Io(e) => write!(f, "{}", e),
|
|
||||||
Self::DefaultDataDirNotFound => write!(
|
|
||||||
f,
|
|
||||||
"Not data directory was specified and a default path could not be determined for this platform."
|
|
||||||
),
|
|
||||||
Self::DatadirCreation(dir_path, e) => write!(
|
|
||||||
f,
|
|
||||||
"Could not create data directory at '{}': '{}'", dir_path.display(), e
|
|
||||||
),
|
|
||||||
Self::MissingBitcoindConfig => write!(
|
|
||||||
f,
|
|
||||||
"Our Bitcoin interface is bitcoind but we have no 'bitcoind_config' entry in the configuration."
|
|
||||||
),
|
|
||||||
Self::MissingElectrumConfig => write!(
|
|
||||||
f,
|
|
||||||
"Our Bitcoin interface is Electrum but we have no 'electrum_config' entry in the configuration."
|
|
||||||
),
|
|
||||||
Self::MissingBitcoinBackendConfig => write!(
|
|
||||||
f,
|
|
||||||
"No Bitcoin backend entry in the configuration."
|
|
||||||
),
|
|
||||||
Self::DbMigrateBitcoinTxs(msg) => write!(
|
|
||||||
f,
|
|
||||||
"Error when migrating Bitcoin transaction from Bitcoin backend to database: {}.", msg
|
|
||||||
),
|
|
||||||
Self::Database(e) => write!(f, "Error initializing database: '{}'.", e),
|
|
||||||
Self::Bitcoind(e) => write!(f, "Error setting up bitcoind interface: '{}'.", e),
|
|
||||||
Self::Electrum(e) => write!(f, "Error setting up Electrum interface: '{}'.", e),
|
|
||||||
#[cfg(windows)]
|
|
||||||
Self::NoWatchonlyInDatadir => {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"A data directory exists with no watchonly wallet. Really old versions of Liana used to not \
|
|
||||||
store the bitcoind watchonly wallet under their own datadir on Windows. A migration will be \
|
|
||||||
necessary to be able to use such an old datadir with recent versions of Liana. The migration \
|
|
||||||
is automatically performed by Liana version 4 and older. If you want to salvage this datadir \
|
|
||||||
first run Liana v4 before running more recent Liana versions."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl error::Error for StartupError {}
|
|
||||||
|
|
||||||
impl From<io::Error> for StartupError {
|
|
||||||
fn from(e: io::Error) -> Self {
|
|
||||||
Self::Io(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SqliteDbError> for StartupError {
|
|
||||||
fn from(e: SqliteDbError) -> Self {
|
|
||||||
Self::Database(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<BitcoindError> for StartupError {
|
|
||||||
fn from(e: BitcoindError) -> Self {
|
|
||||||
Self::Bitcoind(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_datadir(datadir_path: &path::Path) -> Result<(), StartupError> {
|
|
||||||
#[cfg(unix)]
|
|
||||||
return {
|
|
||||||
use fs::DirBuilder;
|
|
||||||
use std::os::unix::fs::DirBuilderExt;
|
|
||||||
|
|
||||||
let mut builder = DirBuilder::new();
|
|
||||||
builder
|
|
||||||
.mode(0o700)
|
|
||||||
.recursive(true)
|
|
||||||
.create(datadir_path)
|
|
||||||
.map_err(|e| StartupError::DatadirCreation(datadir_path.to_path_buf(), e))
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: permissions on Windows..
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
return {
|
|
||||||
fs::create_dir_all(datadir_path)
|
|
||||||
.map_err(|e| StartupError::DatadirCreation(datadir_path.to_path_buf(), e))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect to the SQLite database. Create it if starting fresh, and do some sanity checks.
|
|
||||||
// If all went well, returns the interface to the SQLite database.
|
|
||||||
fn setup_sqlite(
|
|
||||||
config: &Config,
|
|
||||||
data_dir: &path::Path,
|
|
||||||
fresh_data_dir: bool,
|
|
||||||
secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
|
|
||||||
bitcoind: &Option<BitcoinD>,
|
|
||||||
) -> Result<SqliteDb, StartupError> {
|
|
||||||
let db_path: path::PathBuf = [data_dir, path::Path::new("lianad.sqlite3")]
|
|
||||||
.iter()
|
|
||||||
.collect();
|
|
||||||
let options = if fresh_data_dir {
|
|
||||||
Some(FreshDbOptions::new(
|
|
||||||
config.bitcoin_config.network,
|
|
||||||
config.main_descriptor.clone(),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// If opening an existing wallet whose database does not yet store the wallet transactions,
|
|
||||||
// query them from the Bitcoin backend before proceeding to the migration.
|
|
||||||
let sqlite = SqliteDb::new(db_path, options, secp)?;
|
|
||||||
if !fresh_data_dir {
|
|
||||||
let mut conn = sqlite.connection()?;
|
|
||||||
let wallet_txs = if conn.db_version() <= MAX_DB_VERSION_NO_TX_DB {
|
|
||||||
let bit = bitcoind.as_ref().ok_or(StartupError::DbMigrateBitcoinTxs(
|
|
||||||
"a connection to a Bitcoin backend is required",
|
|
||||||
))?;
|
|
||||||
let coins = conn.db_coins(&[]);
|
|
||||||
let coins_txids = coins
|
|
||||||
.iter()
|
|
||||||
.map(|c| c.outpoint.txid)
|
|
||||||
.chain(coins.iter().filter_map(|c| c.spend_txid))
|
|
||||||
.collect::<collections::HashSet<_>>();
|
|
||||||
coins_txids
|
|
||||||
.into_iter()
|
|
||||||
.map(|txid| bit.get_transaction(&txid).map(|res| res.tx))
|
|
||||||
.collect::<Option<Vec<_>>>()
|
|
||||||
.ok_or(StartupError::DbMigrateBitcoinTxs(
|
|
||||||
"missing transaction in Bitcoin backend",
|
|
||||||
))?
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
};
|
|
||||||
sqlite.maybe_apply_migrations(&wallet_txs)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
sqlite.sanity_check(config.bitcoin_config.network, &config.main_descriptor)?;
|
|
||||||
log::info!("Database initialized and checked.");
|
|
||||||
|
|
||||||
Ok(sqlite)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect to bitcoind. Setup the watchonly wallet, and do some sanity checks.
|
|
||||||
// If all went well, returns the interface to bitcoind.
|
|
||||||
fn setup_bitcoind(
|
|
||||||
config: &Config,
|
|
||||||
data_dir: &path::Path,
|
|
||||||
fresh_data_dir: bool,
|
|
||||||
) -> Result<BitcoinD, StartupError> {
|
|
||||||
let wo_path: path::PathBuf = [data_dir, path::Path::new("lianad_watchonly_wallet")]
|
|
||||||
.iter()
|
|
||||||
.collect();
|
|
||||||
let wo_path_str = wo_path.to_str().expect("Must be valid unicode").to_string();
|
|
||||||
// NOTE: On Windows, paths are canonicalized with a "\\?\" prefix to tell Windows to interpret
|
|
||||||
// the string "as is" and to ignore the maximum size of a path. HOWEVER this is not properly
|
|
||||||
// handled by most implementations of the C++ STL's std::filesystem. Therefore bitcoind would
|
|
||||||
// fail to find the wallet if we didn't strip this prefix. It's not ideal, but a lesser evil
|
|
||||||
// than other workarounds i could think about.
|
|
||||||
// See https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces
|
|
||||||
// about the prefix.
|
|
||||||
// See https://stackoverflow.com/questions/71590689/how-to-properly-handle-windows-paths-with-the-long-path-prefix-with-stdfilesys
|
|
||||||
// for a discussion of how one C++ STL implementation handles this.
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
let wo_path_str = wo_path_str.replace("\\\\?\\", "").replace("\\\\?", "");
|
|
||||||
|
|
||||||
let bitcoind_config = match config.bitcoin_backend.as_ref() {
|
|
||||||
Some(config::BitcoinBackend::Bitcoind(bitcoind_config)) => bitcoind_config,
|
|
||||||
_ => Err(StartupError::MissingBitcoindConfig)?,
|
|
||||||
};
|
|
||||||
let bitcoind = BitcoinD::new(bitcoind_config, wo_path_str)?;
|
|
||||||
bitcoind.node_sanity_checks(
|
|
||||||
config.bitcoin_config.network,
|
|
||||||
config.main_descriptor.is_taproot(),
|
|
||||||
)?;
|
|
||||||
if fresh_data_dir {
|
|
||||||
log::info!("Creating a new watchonly wallet on bitcoind.");
|
|
||||||
bitcoind.create_watchonly_wallet(&config.main_descriptor)?;
|
|
||||||
log::info!("Watchonly wallet created.");
|
|
||||||
} else {
|
|
||||||
#[cfg(windows)]
|
|
||||||
if !cfg!(test) && !wo_path.exists() {
|
|
||||||
return Err(StartupError::NoWatchonlyInDatadir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log::info!("Loading our watchonly wallet on bitcoind.");
|
|
||||||
bitcoind.maybe_load_watchonly_wallet()?;
|
|
||||||
bitcoind.wallet_sanity_checks(&config.main_descriptor)?;
|
|
||||||
log::info!("Watchonly wallet loaded on bitcoind and sanity checked.");
|
|
||||||
|
|
||||||
Ok(bitcoind)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create an Electrum interface from a client and BDK-based wallet, and do some sanity checks.
|
|
||||||
// If all went well, returns the interface to Electrum.
|
|
||||||
fn setup_electrum(
|
|
||||||
config: &Config,
|
|
||||||
db: sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
|
|
||||||
) -> Result<Electrum, StartupError> {
|
|
||||||
let electrum_config = match config.bitcoin_backend.as_ref() {
|
|
||||||
Some(config::BitcoinBackend::Electrum(electrum_config)) => electrum_config,
|
|
||||||
_ => Err(StartupError::MissingElectrumConfig)?,
|
|
||||||
};
|
|
||||||
// First create the client to communicate with the Electrum server.
|
|
||||||
let client = electrum::client::Client::new(electrum_config)
|
|
||||||
.map_err(|e| StartupError::Electrum(ElectrumError::Client(e)))?;
|
|
||||||
// Then create the BDK-based wallet and populate it with DB data.
|
|
||||||
let mut db_conn = db.connection();
|
|
||||||
let tip = db_conn.chain_tip();
|
|
||||||
let coins: Vec<_> = db_conn
|
|
||||||
.coins(&[], &[])
|
|
||||||
.into_values()
|
|
||||||
.map(|c| crate::bitcoin::Coin {
|
|
||||||
outpoint: c.outpoint,
|
|
||||||
amount: c.amount,
|
|
||||||
derivation_index: c.derivation_index,
|
|
||||||
is_change: c.is_change,
|
|
||||||
is_immature: c.is_immature,
|
|
||||||
block_info: c.block_info.map(|info| crate::bitcoin::BlockInfo {
|
|
||||||
height: info.height,
|
|
||||||
time: info.time,
|
|
||||||
}),
|
|
||||||
spend_txid: c.spend_txid,
|
|
||||||
spend_block: c.spend_block.map(|info| crate::bitcoin::BlockInfo {
|
|
||||||
height: info.height,
|
|
||||||
time: info.time,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let txids = db_conn.list_saved_txids();
|
|
||||||
// This will only return those txs referenced by our coins, which may not be all of `txids`.
|
|
||||||
let txs: Vec<_> = db_conn
|
|
||||||
.list_wallet_transactions(&txids)
|
|
||||||
.into_iter()
|
|
||||||
.map(|(tx, _, _)| tx)
|
|
||||||
.collect();
|
|
||||||
let (receive_index, change_index) = (db_conn.receive_index(), db_conn.change_index());
|
|
||||||
let genesis_hash = {
|
|
||||||
let chain_hash = ChainHash::using_genesis_block(config.bitcoin_config.network);
|
|
||||||
BlockHash::from_byte_array(*chain_hash.as_bytes())
|
|
||||||
};
|
|
||||||
let bdk_wallet = electrum::wallet::BdkWallet::new(
|
|
||||||
&config.main_descriptor,
|
|
||||||
genesis_hash,
|
|
||||||
tip,
|
|
||||||
&coins,
|
|
||||||
&txs,
|
|
||||||
receive_index,
|
|
||||||
change_index,
|
|
||||||
);
|
|
||||||
let full_scan = db_conn.rescan_timestamp().is_some();
|
|
||||||
let electrum = Electrum::new(client, bdk_wallet, full_scan).map_err(StartupError::Electrum)?;
|
|
||||||
electrum
|
|
||||||
.sanity_checks(&genesis_hash)
|
|
||||||
.map_err(StartupError::Electrum)?;
|
|
||||||
Ok(electrum)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct DaemonControl {
|
|
||||||
config: Config,
|
|
||||||
bitcoin: sync::Arc<sync::Mutex<dyn BitcoinInterface>>,
|
|
||||||
poller_sender: mpsc::SyncSender<poller::PollerMessage>,
|
|
||||||
// FIXME: Should we require Sync on DatabaseInterface rather than using a Mutex?
|
|
||||||
db: sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
|
|
||||||
secp: secp256k1::Secp256k1<secp256k1::VerifyOnly>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DaemonControl {
|
|
||||||
pub(crate) fn new(
|
|
||||||
config: Config,
|
|
||||||
bitcoin: sync::Arc<sync::Mutex<dyn BitcoinInterface>>,
|
|
||||||
poller_sender: mpsc::SyncSender<poller::PollerMessage>,
|
|
||||||
db: sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
|
|
||||||
secp: secp256k1::Secp256k1<secp256k1::VerifyOnly>,
|
|
||||||
) -> DaemonControl {
|
|
||||||
DaemonControl {
|
|
||||||
config,
|
|
||||||
bitcoin,
|
|
||||||
poller_sender,
|
|
||||||
db,
|
|
||||||
secp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Useful for unit test to directly mess up with the DB
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn db(&self) -> sync::Arc<sync::Mutex<dyn DatabaseInterface>> {
|
|
||||||
self.db.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The handle to a Liana daemon. It might either be the handle for a daemon which exposes a
|
|
||||||
/// JSONRPC server or one which exposes its API through a `DaemonControl`.
|
|
||||||
pub enum DaemonHandle {
|
|
||||||
Controller {
|
|
||||||
poller_sender: mpsc::SyncSender<poller::PollerMessage>,
|
|
||||||
poller_handle: thread::JoinHandle<()>,
|
|
||||||
control: DaemonControl,
|
|
||||||
},
|
|
||||||
Server {
|
|
||||||
poller_sender: mpsc::SyncSender<poller::PollerMessage>,
|
|
||||||
poller_handle: thread::JoinHandle<()>,
|
|
||||||
rpcserver_shutdown: sync::Arc<sync::atomic::AtomicBool>,
|
|
||||||
rpcserver_handle: thread::JoinHandle<Result<(), io::Error>>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DaemonHandle {
|
|
||||||
/// This starts the Liana daemon. A user of this interface should regularly poll the `is_alive`
|
|
||||||
/// method to check for internal errors. To shut down the daemon use the `stop` method.
|
|
||||||
///
|
|
||||||
/// The `with_rpc_server` controls whether we should start a JSONRPC server to receive queries
|
|
||||||
/// or instead return a `DaemonControl` object for a caller to access the daemon's API.
|
|
||||||
///
|
|
||||||
/// You may specify a custom Bitcoin interface through the `bitcoin` parameter. If `None`, the
|
|
||||||
/// default Bitcoin interface (`bitcoind` JSONRPC) will be used.
|
|
||||||
/// You may specify a custom Database interface through the `db` parameter. If `None`, the
|
|
||||||
/// default Database interface (SQLite) will be used.
|
|
||||||
pub fn start(
|
|
||||||
config: Config,
|
|
||||||
bitcoin: Option<impl BitcoinInterface + 'static>,
|
|
||||||
db: Option<impl DatabaseInterface + 'static>,
|
|
||||||
with_rpc_server: bool,
|
|
||||||
) -> Result<Self, StartupError> {
|
|
||||||
#[cfg(not(test))]
|
|
||||||
setup_panic_hook();
|
|
||||||
|
|
||||||
let secp = secp256k1::Secp256k1::verification_only();
|
|
||||||
|
|
||||||
// First, check the data directory
|
|
||||||
let mut data_dir = config
|
|
||||||
.data_dir()
|
|
||||||
.ok_or(StartupError::DefaultDataDirNotFound)?;
|
|
||||||
data_dir.push(config.bitcoin_config.network.to_string());
|
|
||||||
let fresh_data_dir = !data_dir.as_path().exists();
|
|
||||||
if fresh_data_dir {
|
|
||||||
create_datadir(&data_dir)?;
|
|
||||||
log::info!("Created a new data directory at '{}'", data_dir.display());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up the connection to bitcoind (if using it) first as we may need it for the database
|
|
||||||
// migration when setting up SQLite below.
|
|
||||||
let bitcoind = if bitcoin.is_none() {
|
|
||||||
if let Some(config::BitcoinBackend::Bitcoind(_)) = &config.bitcoin_backend {
|
|
||||||
Some(setup_bitcoind(&config, &data_dir, fresh_data_dir)?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// Then set up the database backend.
|
|
||||||
let db = match db {
|
|
||||||
Some(db) => sync::Arc::from(sync::Mutex::from(db)),
|
|
||||||
None => sync::Arc::from(sync::Mutex::from(setup_sqlite(
|
|
||||||
&config,
|
|
||||||
&data_dir,
|
|
||||||
fresh_data_dir,
|
|
||||||
&secp,
|
|
||||||
&bitcoind,
|
|
||||||
)?)) as sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Finally set up the Bitcoin backend.
|
|
||||||
let bit = match (bitcoin, &config.bitcoin_backend) {
|
|
||||||
(Some(bit), _) => sync::Arc::from(sync::Mutex::from(bit)),
|
|
||||||
(None, Some(config::BitcoinBackend::Bitcoind(..))) => sync::Arc::from(
|
|
||||||
sync::Mutex::from(bitcoind.expect("bitcoind must have been set already")),
|
|
||||||
)
|
|
||||||
as sync::Arc<sync::Mutex<dyn BitcoinInterface>>,
|
|
||||||
(None, Some(config::BitcoinBackend::Electrum(..))) => {
|
|
||||||
sync::Arc::from(sync::Mutex::from(setup_electrum(&config, db.clone())?))
|
|
||||||
}
|
|
||||||
(None, None) => Err(StartupError::MissingBitcoinBackendConfig)?,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start the poller thread. Keep the thread handle to be able to check if it crashed. Store
|
|
||||||
// an atomic to be able to stop it.
|
|
||||||
let mut bitcoin_poller =
|
|
||||||
poller::Poller::new(bit.clone(), db.clone(), config.main_descriptor.clone());
|
|
||||||
let (poller_sender, poller_receiver) = mpsc::sync_channel(0);
|
|
||||||
let poller_handle = thread::Builder::new()
|
|
||||||
.name("Bitcoin Network poller".to_string())
|
|
||||||
.spawn({
|
|
||||||
let poll_interval = config.bitcoin_config.poll_interval_secs;
|
|
||||||
move || {
|
|
||||||
log::info!("Bitcoin poller started.");
|
|
||||||
bitcoin_poller.poll_forever(poll_interval, poller_receiver);
|
|
||||||
log::info!("Bitcoin poller stopped.");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.expect("Spawning the poller thread must never fail.");
|
|
||||||
|
|
||||||
// Create the API the external world will use to talk to us, either directly through the Rust
|
|
||||||
// structure or through the JSONRPC server we may setup below.
|
|
||||||
let control = DaemonControl::new(config, bit, poller_sender.clone(), db, secp);
|
|
||||||
|
|
||||||
if with_rpc_server {
|
|
||||||
let rpcserver_shutdown = sync::Arc::from(sync::atomic::AtomicBool::from(false));
|
|
||||||
let rpcserver_handle = thread::Builder::new()
|
|
||||||
.name("Bitcoin Network poller".to_string())
|
|
||||||
.spawn({
|
|
||||||
let shutdown = rpcserver_shutdown.clone();
|
|
||||||
move || {
|
|
||||||
let mut rpc_socket = data_dir;
|
|
||||||
rpc_socket.push("lianad_rpc");
|
|
||||||
server::run(&rpc_socket, control, shutdown)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.expect("Spawning the RPC server thread should never fail.");
|
|
||||||
|
|
||||||
return Ok(DaemonHandle::Server {
|
|
||||||
poller_sender,
|
|
||||||
poller_handle,
|
|
||||||
rpcserver_shutdown,
|
|
||||||
rpcserver_handle,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(DaemonHandle::Controller {
|
|
||||||
poller_sender,
|
|
||||||
poller_handle,
|
|
||||||
control,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Start the Liana daemon with the default Bitcoin and database interfaces (`bitcoind` RPC
|
|
||||||
/// and SQLite).
|
|
||||||
pub fn start_default(
|
|
||||||
config: Config,
|
|
||||||
with_rpc_server: bool,
|
|
||||||
) -> Result<DaemonHandle, StartupError> {
|
|
||||||
Self::start(
|
|
||||||
config,
|
|
||||||
Option::<BitcoinD>::None,
|
|
||||||
Option::<SqliteDb>::None,
|
|
||||||
with_rpc_server,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check whether the daemon is still up and running. This needs to be regularly polled to
|
|
||||||
/// check for internal errors. If this returns `false`, collect the error using the `stop`
|
|
||||||
/// method.
|
|
||||||
pub fn is_alive(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
Self::Controller {
|
|
||||||
ref poller_handle, ..
|
|
||||||
} => !poller_handle.is_finished(),
|
|
||||||
Self::Server {
|
|
||||||
ref poller_handle,
|
|
||||||
ref rpcserver_handle,
|
|
||||||
..
|
|
||||||
} => !poller_handle.is_finished() && !rpcserver_handle.is_finished(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stop the Liana daemon. This returns any error which may have occurred.
|
|
||||||
pub fn stop(self) -> Result<(), Box<dyn error::Error>> {
|
|
||||||
match self {
|
|
||||||
Self::Controller {
|
|
||||||
poller_sender,
|
|
||||||
poller_handle,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
poller_sender
|
|
||||||
.send(poller::PollerMessage::Shutdown)
|
|
||||||
.expect("The other end should never have hung up before this.");
|
|
||||||
poller_handle.join().expect("Poller thread must not panic");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Self::Server {
|
|
||||||
poller_sender,
|
|
||||||
poller_handle,
|
|
||||||
rpcserver_shutdown,
|
|
||||||
rpcserver_handle,
|
|
||||||
} => {
|
|
||||||
poller_sender
|
|
||||||
.send(poller::PollerMessage::Shutdown)
|
|
||||||
.expect("The other end should never have hung up before this.");
|
|
||||||
rpcserver_shutdown.store(true, sync::atomic::Ordering::Relaxed);
|
|
||||||
rpcserver_handle
|
|
||||||
.join()
|
|
||||||
.expect("Poller thread must not panic")?;
|
|
||||||
poller_handle.join().expect("Poller thread must not panic");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(all(test, unix))]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::{
|
|
||||||
config::{BitcoinConfig, BitcoindConfig, BitcoindRpcAuth},
|
|
||||||
descriptors::LianaDescriptor,
|
|
||||||
testutils::*,
|
|
||||||
};
|
|
||||||
|
|
||||||
use miniscript::bitcoin;
|
|
||||||
use std::{
|
|
||||||
fs,
|
|
||||||
io::{BufRead, BufReader, Write},
|
|
||||||
net, path,
|
|
||||||
str::FromStr,
|
|
||||||
thread, time,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Read all bytes from the socket until the end of a JSON object, good enough approximation.
|
|
||||||
fn read_til_json_end(stream: &mut net::TcpStream) {
|
|
||||||
stream
|
|
||||||
.set_read_timeout(Some(time::Duration::from_secs(5)))
|
|
||||||
.unwrap();
|
|
||||||
let mut reader = BufReader::new(stream);
|
|
||||||
loop {
|
|
||||||
let mut line = String::new();
|
|
||||||
reader.read_line(&mut line).unwrap();
|
|
||||||
|
|
||||||
if line.starts_with("Authorization") {
|
|
||||||
let mut buf = vec![0; 256];
|
|
||||||
reader.read_until(b'}', &mut buf).unwrap();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Respond to the two "echo" sent at startup to sanity check the connection
|
|
||||||
fn complete_sanity_check(server: &net::TcpListener) {
|
|
||||||
let echo_resp =
|
|
||||||
"HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[]}\n".as_bytes();
|
|
||||||
|
|
||||||
// Read the first echo, respond to it
|
|
||||||
{
|
|
||||||
let (mut stream, _) = server.accept().unwrap();
|
|
||||||
read_til_json_end(&mut stream);
|
|
||||||
stream.write_all(echo_resp).unwrap();
|
|
||||||
stream.flush().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the second echo, respond to it
|
|
||||||
let (mut stream, _) = server.accept().unwrap();
|
|
||||||
read_til_json_end(&mut stream);
|
|
||||||
stream.write_all(echo_resp).unwrap();
|
|
||||||
stream.flush().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send them a pruned getblockchaininfo telling them we are at version 24.0
|
|
||||||
fn complete_version_check(server: &net::TcpListener) {
|
|
||||||
let net_resp =
|
|
||||||
"HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"version\":240000}}\n"
|
|
||||||
.as_bytes();
|
|
||||||
let (mut stream, _) = server.accept().unwrap();
|
|
||||||
read_til_json_end(&mut stream);
|
|
||||||
stream.write_all(net_resp).unwrap();
|
|
||||||
stream.flush().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send them a pruned getblockchaininfo telling them we are on mainnet
|
|
||||||
fn complete_network_check(server: &net::TcpListener) {
|
|
||||||
let net_resp =
|
|
||||||
"HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"chain\":\"main\"}}\n"
|
|
||||||
.as_bytes();
|
|
||||||
let (mut stream, _) = server.accept().unwrap();
|
|
||||||
read_til_json_end(&mut stream);
|
|
||||||
stream.write_all(net_resp).unwrap();
|
|
||||||
stream.flush().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send them responses for the calls involved when creating a fresh wallet
|
|
||||||
fn complete_wallet_creation(server: &net::TcpListener) {
|
|
||||||
{
|
|
||||||
let net_resp =
|
|
||||||
["HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[]}\n".as_bytes()]
|
|
||||||
.concat();
|
|
||||||
let (mut stream, _) = server.accept().unwrap();
|
|
||||||
read_til_json_end(&mut stream);
|
|
||||||
stream.write_all(&net_resp).unwrap();
|
|
||||||
stream.flush().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let net_resp = [
|
|
||||||
"HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"name\":\"dummy\"}}\n"
|
|
||||||
.as_bytes(),
|
|
||||||
]
|
|
||||||
.concat();
|
|
||||||
let (mut stream, _) = server.accept().unwrap();
|
|
||||||
read_til_json_end(&mut stream);
|
|
||||||
stream.write_all(&net_resp).unwrap();
|
|
||||||
stream.flush().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let net_resp = [
|
|
||||||
"HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[{\"success\":true}]}\n"
|
|
||||||
.as_bytes(),
|
|
||||||
]
|
|
||||||
.concat();
|
|
||||||
let (mut stream, _) = server.accept().unwrap();
|
|
||||||
read_til_json_end(&mut stream);
|
|
||||||
stream.write_all(&net_resp).unwrap();
|
|
||||||
stream.flush().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send them a dummy result to loadwallet.
|
|
||||||
fn complete_wallet_loading(server: &net::TcpListener) {
|
|
||||||
{
|
|
||||||
let listwallets_resp =
|
|
||||||
"HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[]}\n".as_bytes();
|
|
||||||
let (mut stream, _) = server.accept().unwrap();
|
|
||||||
read_til_json_end(&mut stream);
|
|
||||||
stream.write_all(listwallets_resp).unwrap();
|
|
||||||
stream.flush().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let loadwallet_resp =
|
|
||||||
"HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"name\":\"dummy\"}}\n"
|
|
||||||
.as_bytes();
|
|
||||||
let (mut stream, _) = server.accept().unwrap();
|
|
||||||
read_til_json_end(&mut stream);
|
|
||||||
stream.write_all(loadwallet_resp).unwrap();
|
|
||||||
stream.flush().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send them a response to 'listwallets' with the watchonly wallet path
|
|
||||||
fn complete_wallet_check(server: &net::TcpListener, watchonly_wallet_path: &str) {
|
|
||||||
let net_resp = [
|
|
||||||
"HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[\"".as_bytes(),
|
|
||||||
watchonly_wallet_path.as_bytes(),
|
|
||||||
"\"]}\n".as_bytes(),
|
|
||||||
]
|
|
||||||
.concat();
|
|
||||||
let (mut stream, _) = server.accept().unwrap();
|
|
||||||
read_til_json_end(&mut stream);
|
|
||||||
stream.write_all(&net_resp).unwrap();
|
|
||||||
stream.flush().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send them a response to 'listdescriptors' with the receive and change descriptors
|
|
||||||
fn complete_desc_check(server: &net::TcpListener, receive_desc: &str, change_desc: &str) {
|
|
||||||
let net_resp = [
|
|
||||||
"HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"descriptors\":[{\"desc\":\"".as_bytes(),
|
|
||||||
receive_desc.as_bytes(),
|
|
||||||
"\",\"timestamp\":0},".as_bytes(),
|
|
||||||
"{\"desc\":\"".as_bytes(),
|
|
||||||
change_desc.as_bytes(),
|
|
||||||
"\",\"timestamp\":1}]}}\n".as_bytes(),
|
|
||||||
]
|
|
||||||
.concat();
|
|
||||||
let (mut stream, _) = server.accept().unwrap();
|
|
||||||
read_til_json_end(&mut stream);
|
|
||||||
stream.write_all(&net_resp).unwrap();
|
|
||||||
stream.flush().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send them a response to 'getblockhash' with the genesis block hash
|
|
||||||
fn complete_tip_init(server: &net::TcpListener) {
|
|
||||||
let net_resp = [
|
|
||||||
"HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f\"}\n".as_bytes(),
|
|
||||||
]
|
|
||||||
.concat();
|
|
||||||
let (mut stream, _) = server.accept().unwrap();
|
|
||||||
read_til_json_end(&mut stream);
|
|
||||||
stream.write_all(&net_resp).unwrap();
|
|
||||||
stream.flush().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: we could move the dummy bitcoind thread stuff to the bitcoind module to test the
|
|
||||||
// bitcoind interface, and use the DummyLiana from testutils to sanity check the startup.
|
|
||||||
// Note that startup as checked by this unit test is also tested in the functional test
|
|
||||||
// framework.
|
|
||||||
#[test]
|
|
||||||
fn daemon_startup() {
|
|
||||||
let tmp_dir = tmp_dir();
|
|
||||||
fs::create_dir_all(&tmp_dir).unwrap();
|
|
||||||
let data_dir: path::PathBuf = [tmp_dir.as_path(), path::Path::new("datadir")]
|
|
||||||
.iter()
|
|
||||||
.collect();
|
|
||||||
let wo_path: path::PathBuf = [
|
|
||||||
data_dir.as_path(),
|
|
||||||
path::Path::new("bitcoin"),
|
|
||||||
path::Path::new("lianad_watchonly_wallet"),
|
|
||||||
]
|
|
||||||
.iter()
|
|
||||||
.collect();
|
|
||||||
let wo_path = wo_path.to_str().unwrap().to_string();
|
|
||||||
|
|
||||||
// Configure a dummy bitcoind
|
|
||||||
let network = bitcoin::Network::Bitcoin;
|
|
||||||
let cookie: path::PathBuf = [
|
|
||||||
tmp_dir.as_path(),
|
|
||||||
path::Path::new(&format!(
|
|
||||||
"dummy_bitcoind_{:?}.cookie",
|
|
||||||
thread::current().id()
|
|
||||||
)),
|
|
||||||
]
|
|
||||||
.iter()
|
|
||||||
.collect();
|
|
||||||
fs::write(&cookie, [0; 32]).unwrap(); // Will overwrite should it exist already
|
|
||||||
let addr: net::SocketAddr =
|
|
||||||
net::SocketAddrV4::new(net::Ipv4Addr::new(127, 0, 0, 1), 0).into();
|
|
||||||
let server = net::TcpListener::bind(addr).unwrap();
|
|
||||||
let addr = server.local_addr().unwrap();
|
|
||||||
let bitcoin_config = BitcoinConfig {
|
|
||||||
network,
|
|
||||||
poll_interval_secs: time::Duration::from_secs(2),
|
|
||||||
};
|
|
||||||
let bitcoind_config = BitcoindConfig {
|
|
||||||
addr,
|
|
||||||
rpc_auth: BitcoindRpcAuth::CookieFile(cookie),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create a dummy config with this bitcoind
|
|
||||||
let desc_str = "wsh(andor(pk([aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/<0;1>/*),older(10000),pk([aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/<0;1>/*)))#3xh8xmhn";
|
|
||||||
let desc = LianaDescriptor::from_str(desc_str).unwrap();
|
|
||||||
let receive_desc = desc.receive_descriptor().clone();
|
|
||||||
let change_desc = desc.change_descriptor().clone();
|
|
||||||
let config = Config {
|
|
||||||
bitcoin_config,
|
|
||||||
bitcoin_backend: Some(config::BitcoinBackend::Bitcoind(bitcoind_config)),
|
|
||||||
data_dir: Some(data_dir),
|
|
||||||
log_level: log::LevelFilter::Debug,
|
|
||||||
main_descriptor: desc,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start the daemon in a new thread so the current one acts as the bitcoind server.
|
|
||||||
let t = thread::spawn({
|
|
||||||
let config = config.clone();
|
|
||||||
move || {
|
|
||||||
let handle = DaemonHandle::start_default(config, false).unwrap();
|
|
||||||
handle.stop().unwrap();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
complete_sanity_check(&server);
|
|
||||||
complete_version_check(&server);
|
|
||||||
complete_network_check(&server);
|
|
||||||
complete_wallet_creation(&server);
|
|
||||||
complete_wallet_loading(&server);
|
|
||||||
complete_wallet_check(&server, &wo_path);
|
|
||||||
complete_desc_check(&server, &receive_desc.to_string(), &change_desc.to_string());
|
|
||||||
complete_tip_init(&server);
|
|
||||||
// We don't have to complete the sync check as the poller checks whether it needs to stop
|
|
||||||
// before checking the bitcoind sync status.
|
|
||||||
t.join().unwrap();
|
|
||||||
|
|
||||||
// The datadir is created now, so if we restart it it won't create the wo wallet.
|
|
||||||
let t = thread::spawn({
|
|
||||||
let config = config.clone();
|
|
||||||
move || {
|
|
||||||
let handle = DaemonHandle::start_default(config, false).unwrap();
|
|
||||||
handle.stop().unwrap();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
complete_sanity_check(&server);
|
|
||||||
complete_version_check(&server);
|
|
||||||
complete_network_check(&server);
|
|
||||||
complete_wallet_loading(&server);
|
|
||||||
complete_wallet_check(&server, &wo_path);
|
|
||||||
complete_desc_check(&server, &receive_desc.to_string(), &change_desc.to_string());
|
|
||||||
// We don't have to complete the sync check as the poller checks whether it needs to stop
|
|
||||||
// before checking the bitcoind sync status.
|
|
||||||
t.join().unwrap();
|
|
||||||
|
|
||||||
fs::remove_dir_all(&tmp_dir).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -407,13 +407,30 @@ impl HotSigner {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{descriptors, testutils::*};
|
use crate::descriptors;
|
||||||
use miniscript::{
|
use miniscript::{
|
||||||
bitcoin::{locktime::absolute, psbt::Input as PsbtIn, Amount},
|
bitcoin::{locktime::absolute, psbt::Input as PsbtIn, Amount},
|
||||||
descriptor::{DerivPaths, DescriptorMultiXKey, DescriptorPublicKey, Wildcard},
|
descriptor::{DerivPaths, DescriptorMultiXKey, DescriptorPublicKey, Wildcard},
|
||||||
};
|
};
|
||||||
use std::collections::{BTreeMap, HashSet};
|
use std::collections::{BTreeMap, HashSet};
|
||||||
|
|
||||||
|
static mut COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
|
||||||
|
fn uid() -> usize {
|
||||||
|
unsafe {
|
||||||
|
let uid = COUNTER.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
uid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn tmp_dir() -> path::PathBuf {
|
||||||
|
std::env::temp_dir().join(format!(
|
||||||
|
"lianad-{}-{:?}-{}",
|
||||||
|
std::process::id(),
|
||||||
|
std::thread::current().id(),
|
||||||
|
uid(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hot_signer_gen() {
|
fn hot_signer_gen() {
|
||||||
// Entropy isn't completely broken.
|
// Entropy isn't completely broken.
|
||||||
|
|||||||
54
lianad/Cargo.toml
Normal file
54
lianad/Cargo.toml
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
[package]
|
||||||
|
name = "lianad"
|
||||||
|
version = "8.0.0"
|
||||||
|
authors = ["Antoine Poinsot <darosior@protonmail.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
repository = "https://github.com/wizardsardine/liana"
|
||||||
|
license-file = "LICENCE"
|
||||||
|
keywords = ["bitcoin", "wallet", "miniscript", "inheritance", "recovery"]
|
||||||
|
description = "Liana wallet daemon"
|
||||||
|
exclude = [".github/", ".cirrus.yml", "tests/", "test_data/", "contrib/", "pyproject.toml"]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "lianad"
|
||||||
|
path = "src/bin/daemon.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "liana-cli"
|
||||||
|
path = "src/bin/cli.rs"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
nonblocking_shutdown = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
liana = { path = "../liana" }
|
||||||
|
# For managing transactions (it re-exports the bitcoin crate)
|
||||||
|
miniscript = { version = "11.0", features = ["serde", "compiler", "base64"] }
|
||||||
|
|
||||||
|
# For Electrum backend. This is the latest version with the same bitcoin version as
|
||||||
|
# the miniscript dependency.
|
||||||
|
bdk_electrum = { version = "0.14" }
|
||||||
|
|
||||||
|
# Don't reinvent the wheel
|
||||||
|
dirs = "5.0"
|
||||||
|
|
||||||
|
# We use TOML for the config, and JSON for RPC
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
toml = "0.5"
|
||||||
|
serde_json = { version = "1.0", features = ["raw_value"] }
|
||||||
|
|
||||||
|
# Logging stuff
|
||||||
|
log = "0.4"
|
||||||
|
fern = "0.6"
|
||||||
|
|
||||||
|
# In order to have a backtrace on panic, because the
|
||||||
|
# stdlib does not have a programmatic interface yet
|
||||||
|
# to work with our custom panic hook.
|
||||||
|
backtrace = "0.3"
|
||||||
|
|
||||||
|
# Pinned to this version because they keep breaking their MSRV in point releases...
|
||||||
|
# FIXME: this is unfortunate, we don't receive the updates (sometimes critical) from SQLite.
|
||||||
|
rusqlite = { version = "0.30", features = ["bundled", "unlock_notify"] }
|
||||||
|
|
||||||
|
# To talk to bitcoind
|
||||||
|
jsonrpc = { version = "0.17", features = ["minreq_http"], default-features = false }
|
||||||
@ -1,6 +1,6 @@
|
|||||||
#![cfg(not(target_os = "windows"))]
|
#![cfg(not(target_os = "windows"))]
|
||||||
|
|
||||||
use liana::config::{config_folder_path, Config};
|
use lianad::config::{config_folder_path, Config};
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
@ -5,7 +5,7 @@ use std::{
|
|||||||
process, thread, time,
|
process, thread, time,
|
||||||
};
|
};
|
||||||
|
|
||||||
use liana::{config::Config, DaemonHandle, VERSION};
|
use lianad::{config::Config, DaemonHandle, VERSION};
|
||||||
|
|
||||||
fn print_help_exit(code: i32) {
|
fn print_help_exit(code: i32) {
|
||||||
eprintln!("lianad version {}", VERSION);
|
eprintln!("lianad version {}", VERSION);
|
||||||
@ -6,8 +6,8 @@ mod utils;
|
|||||||
use crate::{
|
use crate::{
|
||||||
bitcoin::{Block, BlockChainTip},
|
bitcoin::{Block, BlockChainTip},
|
||||||
config,
|
config,
|
||||||
descriptors::LianaDescriptor,
|
|
||||||
};
|
};
|
||||||
|
use liana::descriptors::LianaDescriptor;
|
||||||
use utils::{block_before_date, roundup_progress};
|
use utils::{block_before_date, roundup_progress};
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
@ -17,10 +17,8 @@ use miniscript::bitcoin::bip32::ChildNumber;
|
|||||||
use super::utils::{
|
use super::utils::{
|
||||||
block_id_from_tip, block_info_from_anchor, height_i32_from_u32, height_u32_from_i32,
|
block_id_from_tip, block_info_from_anchor, height_i32_from_u32, height_u32_from_i32,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::bitcoin::{Block, BlockChainTip, Coin, COINBASE_MATURITY};
|
||||||
bitcoin::{Block, BlockChainTip, Coin, COINBASE_MATURITY},
|
use liana::descriptors::LianaDescriptor;
|
||||||
descriptors::LianaDescriptor,
|
|
||||||
};
|
|
||||||
|
|
||||||
// We don't want to overload the server (each SPK is separate call).
|
// We don't want to overload the server (each SPK is separate call).
|
||||||
const LOOK_AHEAD_LIMIT: u32 = 30;
|
const LOOK_AHEAD_LIMIT: u32 = 30;
|
||||||
@ -6,11 +6,9 @@ pub mod d;
|
|||||||
pub mod electrum;
|
pub mod electrum;
|
||||||
pub mod poller;
|
pub mod poller;
|
||||||
|
|
||||||
use crate::{
|
use crate::bitcoin::d::{BitcoindError, CachedTxGetter, LSBlockEntry};
|
||||||
bitcoin::d::{BitcoindError, CachedTxGetter, LSBlockEntry},
|
|
||||||
descriptors,
|
|
||||||
};
|
|
||||||
pub use d::{MempoolEntry, MempoolEntryFees, SyncProgress};
|
pub use d::{MempoolEntry, MempoolEntryFees, SyncProgress};
|
||||||
|
use liana::descriptors;
|
||||||
|
|
||||||
use std::{fmt, sync};
|
use std::{fmt, sync};
|
||||||
|
|
||||||
@ -1,11 +1,11 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
bitcoin::{BitcoinInterface, BlockChainTip, UTxO, UTxOAddress},
|
bitcoin::{BitcoinInterface, BlockChainTip, UTxO, UTxOAddress},
|
||||||
database::{Coin, DatabaseConnection, DatabaseInterface},
|
database::{Coin, DatabaseConnection, DatabaseInterface},
|
||||||
descriptors,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::{collections::HashSet, convert::TryInto, sync, thread, time};
|
use std::{collections::HashSet, convert::TryInto, sync, thread, time};
|
||||||
|
|
||||||
|
use liana::descriptors;
|
||||||
use miniscript::bitcoin::{self, secp256k1};
|
use miniscript::bitcoin::{self, secp256k1};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -1,6 +1,7 @@
|
|||||||
mod looper;
|
mod looper;
|
||||||
|
|
||||||
use crate::{bitcoin::BitcoinInterface, database::DatabaseInterface, descriptors};
|
use crate::{bitcoin::BitcoinInterface, database::DatabaseInterface};
|
||||||
|
use liana::descriptors;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
sync::{self, mpsc},
|
sync::{self, mpsc},
|
||||||
@ -7,18 +7,21 @@ mod utils;
|
|||||||
use crate::{
|
use crate::{
|
||||||
bitcoin::BitcoinInterface,
|
bitcoin::BitcoinInterface,
|
||||||
database::{Coin, DatabaseConnection, DatabaseInterface},
|
database::{Coin, DatabaseConnection, DatabaseInterface},
|
||||||
descriptors,
|
|
||||||
miniscript::bitcoin::absolute::LockTime,
|
miniscript::bitcoin::absolute::LockTime,
|
||||||
poller::PollerMessage,
|
poller::PollerMessage,
|
||||||
spend::{
|
|
||||||
self, create_spend, AddrInfo, AncestorInfo, CandidateCoin, CreateSpendRes,
|
|
||||||
SpendCreationError, SpendOutputAddress, SpendTxFees, TxGetter,
|
|
||||||
},
|
|
||||||
DaemonControl, VERSION,
|
DaemonControl, VERSION,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use crate::database::{CoinStatus, LabelItem};
|
pub use crate::database::{CoinStatus, LabelItem};
|
||||||
|
|
||||||
|
use liana::{
|
||||||
|
descriptors,
|
||||||
|
spend::{
|
||||||
|
self, create_spend, AddrInfo, AncestorInfo, CandidateCoin, CreateSpendRes,
|
||||||
|
SpendCreationError, SpendOutputAddress, SpendTxFees, TxGetter,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
use utils::{
|
use utils::{
|
||||||
deser_addr_assume_checked, deser_amount_from_sats, deser_fromstr, deser_hex, ser_amount,
|
deser_addr_assume_checked, deser_amount_from_sats, deser_fromstr, deser_hex, ser_amount,
|
||||||
ser_hex, ser_to_string,
|
ser_hex, ser_to_string,
|
||||||
@ -1280,7 +1283,8 @@ pub struct CreateRecoveryResult {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{bitcoin::Block, database::BlockInfo, spend::InsaneFeeInfo, testutils::*};
|
use crate::{bitcoin::Block, database::BlockInfo, testutils::*};
|
||||||
|
use liana::spend::InsaneFeeInfo;
|
||||||
|
|
||||||
use bitcoin::{
|
use bitcoin::{
|
||||||
bip32::{self, ChildNumber},
|
bip32::{self, ChildNumber},
|
||||||
@ -1,4 +1,4 @@
|
|||||||
use crate::descriptors::LianaDescriptor;
|
use liana::descriptors::LianaDescriptor;
|
||||||
|
|
||||||
use std::{fmt, net::SocketAddr, path::PathBuf, str::FromStr, time::Duration};
|
use std::{fmt, net::SocketAddr, path::PathBuf, str::FromStr, time::Duration};
|
||||||
|
|
||||||
@ -25,8 +25,8 @@ use crate::{
|
|||||||
},
|
},
|
||||||
Coin, CoinStatus, LabelItem,
|
Coin, CoinStatus, LabelItem,
|
||||||
},
|
},
|
||||||
descriptors::LianaDescriptor,
|
|
||||||
};
|
};
|
||||||
|
use liana::descriptors::LianaDescriptor;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
cmp,
|
cmp,
|
||||||
@ -1,4 +1,4 @@
|
|||||||
use crate::descriptors::LianaDescriptor;
|
use liana::descriptors::LianaDescriptor;
|
||||||
|
|
||||||
use std::{convert::TryFrom, str::FromStr};
|
use std::{convert::TryFrom, str::FromStr};
|
||||||
|
|
||||||
870
lianad/src/lib.rs
Normal file
870
lianad/src/lib.rs
Normal file
@ -0,0 +1,870 @@
|
|||||||
|
mod bitcoin;
|
||||||
|
pub mod commands;
|
||||||
|
pub mod config;
|
||||||
|
mod database;
|
||||||
|
mod jsonrpc;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod testutils;
|
||||||
|
|
||||||
|
pub use bdk_electrum::electrum_client;
|
||||||
|
use bitcoin::electrum;
|
||||||
|
pub use miniscript;
|
||||||
|
|
||||||
|
pub use crate::bitcoin::{
|
||||||
|
d::{BitcoinD, BitcoindError, WalletError},
|
||||||
|
electrum::{Electrum, ElectrumError},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::jsonrpc::server;
|
||||||
|
use crate::{
|
||||||
|
bitcoin::{poller, BitcoinInterface},
|
||||||
|
config::Config,
|
||||||
|
database::{
|
||||||
|
sqlite::{FreshDbOptions, SqliteDb, SqliteDbError, MAX_DB_VERSION_NO_TX_DB},
|
||||||
|
DatabaseInterface,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
collections, error, fmt, fs, io, path,
|
||||||
|
sync::{self, mpsc},
|
||||||
|
thread,
|
||||||
|
};
|
||||||
|
|
||||||
|
use miniscript::bitcoin::{constants::ChainHash, hashes::Hash, secp256k1, BlockHash};
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
use std::panic;
|
||||||
|
// A panic in any thread should stop the main thread, and print the panic.
|
||||||
|
#[cfg(not(test))]
|
||||||
|
fn setup_panic_hook() {
|
||||||
|
panic::set_hook(Box::new(move |panic_info| {
|
||||||
|
let file = panic_info
|
||||||
|
.location()
|
||||||
|
.map(|l| l.file())
|
||||||
|
.unwrap_or_else(|| "'unknown'");
|
||||||
|
let line = panic_info
|
||||||
|
.location()
|
||||||
|
.map(|l| l.line().to_string())
|
||||||
|
.unwrap_or_else(|| "'unknown'".to_string());
|
||||||
|
|
||||||
|
let bt = backtrace::Backtrace::new();
|
||||||
|
let info = panic_info
|
||||||
|
.payload()
|
||||||
|
.downcast_ref::<&str>()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.or_else(|| panic_info.payload().downcast_ref::<String>().cloned());
|
||||||
|
log::error!(
|
||||||
|
"panic occurred at line {} of file {}: {:?}\n{:?}",
|
||||||
|
line,
|
||||||
|
file,
|
||||||
|
info,
|
||||||
|
bt
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Version {
|
||||||
|
pub major: u32,
|
||||||
|
pub minor: u32,
|
||||||
|
pub patch: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Version {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}.{}.{}-dev", self.major, self.minor, self.patch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const VERSION: Version = Version {
|
||||||
|
major: 8,
|
||||||
|
minor: 0,
|
||||||
|
patch: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum StartupError {
|
||||||
|
Io(io::Error),
|
||||||
|
DefaultDataDirNotFound,
|
||||||
|
DatadirCreation(path::PathBuf, io::Error),
|
||||||
|
MissingBitcoindConfig,
|
||||||
|
MissingElectrumConfig,
|
||||||
|
MissingBitcoinBackendConfig,
|
||||||
|
DbMigrateBitcoinTxs(&'static str),
|
||||||
|
Database(SqliteDbError),
|
||||||
|
Bitcoind(BitcoindError),
|
||||||
|
Electrum(ElectrumError),
|
||||||
|
#[cfg(windows)]
|
||||||
|
NoWatchonlyInDatadir,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for StartupError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Io(e) => write!(f, "{}", e),
|
||||||
|
Self::DefaultDataDirNotFound => write!(
|
||||||
|
f,
|
||||||
|
"Not data directory was specified and a default path could not be determined for this platform."
|
||||||
|
),
|
||||||
|
Self::DatadirCreation(dir_path, e) => write!(
|
||||||
|
f,
|
||||||
|
"Could not create data directory at '{}': '{}'", dir_path.display(), e
|
||||||
|
),
|
||||||
|
Self::MissingBitcoindConfig => write!(
|
||||||
|
f,
|
||||||
|
"Our Bitcoin interface is bitcoind but we have no 'bitcoind_config' entry in the configuration."
|
||||||
|
),
|
||||||
|
Self::MissingElectrumConfig => write!(
|
||||||
|
f,
|
||||||
|
"Our Bitcoin interface is Electrum but we have no 'electrum_config' entry in the configuration."
|
||||||
|
),
|
||||||
|
Self::MissingBitcoinBackendConfig => write!(
|
||||||
|
f,
|
||||||
|
"No Bitcoin backend entry in the configuration."
|
||||||
|
),
|
||||||
|
Self::DbMigrateBitcoinTxs(msg) => write!(
|
||||||
|
f,
|
||||||
|
"Error when migrating Bitcoin transaction from Bitcoin backend to database: {}.", msg
|
||||||
|
),
|
||||||
|
Self::Database(e) => write!(f, "Error initializing database: '{}'.", e),
|
||||||
|
Self::Bitcoind(e) => write!(f, "Error setting up bitcoind interface: '{}'.", e),
|
||||||
|
Self::Electrum(e) => write!(f, "Error setting up Electrum interface: '{}'.", e),
|
||||||
|
#[cfg(windows)]
|
||||||
|
Self::NoWatchonlyInDatadir => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"A data directory exists with no watchonly wallet. Really old versions of Liana used to not \
|
||||||
|
store the bitcoind watchonly wallet under their own datadir on Windows. A migration will be \
|
||||||
|
necessary to be able to use such an old datadir with recent versions of Liana. The migration \
|
||||||
|
is automatically performed by Liana version 4 and older. If you want to salvage this datadir \
|
||||||
|
first run Liana v4 before running more recent Liana versions."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl error::Error for StartupError {}
|
||||||
|
|
||||||
|
impl From<io::Error> for StartupError {
|
||||||
|
fn from(e: io::Error) -> Self {
|
||||||
|
Self::Io(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SqliteDbError> for StartupError {
|
||||||
|
fn from(e: SqliteDbError) -> Self {
|
||||||
|
Self::Database(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BitcoindError> for StartupError {
|
||||||
|
fn from(e: BitcoindError) -> Self {
|
||||||
|
Self::Bitcoind(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_datadir(datadir_path: &path::Path) -> Result<(), StartupError> {
|
||||||
|
#[cfg(unix)]
|
||||||
|
return {
|
||||||
|
use fs::DirBuilder;
|
||||||
|
use std::os::unix::fs::DirBuilderExt;
|
||||||
|
|
||||||
|
let mut builder = DirBuilder::new();
|
||||||
|
builder
|
||||||
|
.mode(0o700)
|
||||||
|
.recursive(true)
|
||||||
|
.create(datadir_path)
|
||||||
|
.map_err(|e| StartupError::DatadirCreation(datadir_path.to_path_buf(), e))
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: permissions on Windows..
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
return {
|
||||||
|
fs::create_dir_all(datadir_path)
|
||||||
|
.map_err(|e| StartupError::DatadirCreation(datadir_path.to_path_buf(), e))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to the SQLite database. Create it if starting fresh, and do some sanity checks.
|
||||||
|
// If all went well, returns the interface to the SQLite database.
|
||||||
|
fn setup_sqlite(
|
||||||
|
config: &Config,
|
||||||
|
data_dir: &path::Path,
|
||||||
|
fresh_data_dir: bool,
|
||||||
|
secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
|
||||||
|
bitcoind: &Option<BitcoinD>,
|
||||||
|
) -> Result<SqliteDb, StartupError> {
|
||||||
|
let db_path: path::PathBuf = [data_dir, path::Path::new("lianad.sqlite3")]
|
||||||
|
.iter()
|
||||||
|
.collect();
|
||||||
|
let options = if fresh_data_dir {
|
||||||
|
Some(FreshDbOptions::new(
|
||||||
|
config.bitcoin_config.network,
|
||||||
|
config.main_descriptor.clone(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// If opening an existing wallet whose database does not yet store the wallet transactions,
|
||||||
|
// query them from the Bitcoin backend before proceeding to the migration.
|
||||||
|
let sqlite = SqliteDb::new(db_path, options, secp)?;
|
||||||
|
if !fresh_data_dir {
|
||||||
|
let mut conn = sqlite.connection()?;
|
||||||
|
let wallet_txs = if conn.db_version() <= MAX_DB_VERSION_NO_TX_DB {
|
||||||
|
let bit = bitcoind.as_ref().ok_or(StartupError::DbMigrateBitcoinTxs(
|
||||||
|
"a connection to a Bitcoin backend is required",
|
||||||
|
))?;
|
||||||
|
let coins = conn.db_coins(&[]);
|
||||||
|
let coins_txids = coins
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.outpoint.txid)
|
||||||
|
.chain(coins.iter().filter_map(|c| c.spend_txid))
|
||||||
|
.collect::<collections::HashSet<_>>();
|
||||||
|
coins_txids
|
||||||
|
.into_iter()
|
||||||
|
.map(|txid| bit.get_transaction(&txid).map(|res| res.tx))
|
||||||
|
.collect::<Option<Vec<_>>>()
|
||||||
|
.ok_or(StartupError::DbMigrateBitcoinTxs(
|
||||||
|
"missing transaction in Bitcoin backend",
|
||||||
|
))?
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
sqlite.maybe_apply_migrations(&wallet_txs)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite.sanity_check(config.bitcoin_config.network, &config.main_descriptor)?;
|
||||||
|
log::info!("Database initialized and checked.");
|
||||||
|
|
||||||
|
Ok(sqlite)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to bitcoind. Setup the watchonly wallet, and do some sanity checks.
|
||||||
|
// If all went well, returns the interface to bitcoind.
|
||||||
|
fn setup_bitcoind(
|
||||||
|
config: &Config,
|
||||||
|
data_dir: &path::Path,
|
||||||
|
fresh_data_dir: bool,
|
||||||
|
) -> Result<BitcoinD, StartupError> {
|
||||||
|
let wo_path: path::PathBuf = [data_dir, path::Path::new("lianad_watchonly_wallet")]
|
||||||
|
.iter()
|
||||||
|
.collect();
|
||||||
|
let wo_path_str = wo_path.to_str().expect("Must be valid unicode").to_string();
|
||||||
|
// NOTE: On Windows, paths are canonicalized with a "\\?\" prefix to tell Windows to interpret
|
||||||
|
// the string "as is" and to ignore the maximum size of a path. HOWEVER this is not properly
|
||||||
|
// handled by most implementations of the C++ STL's std::filesystem. Therefore bitcoind would
|
||||||
|
// fail to find the wallet if we didn't strip this prefix. It's not ideal, but a lesser evil
|
||||||
|
// than other workarounds i could think about.
|
||||||
|
// See https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces
|
||||||
|
// about the prefix.
|
||||||
|
// See https://stackoverflow.com/questions/71590689/how-to-properly-handle-windows-paths-with-the-long-path-prefix-with-stdfilesys
|
||||||
|
// for a discussion of how one C++ STL implementation handles this.
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let wo_path_str = wo_path_str.replace("\\\\?\\", "").replace("\\\\?", "");
|
||||||
|
|
||||||
|
let bitcoind_config = match config.bitcoin_backend.as_ref() {
|
||||||
|
Some(config::BitcoinBackend::Bitcoind(bitcoind_config)) => bitcoind_config,
|
||||||
|
_ => Err(StartupError::MissingBitcoindConfig)?,
|
||||||
|
};
|
||||||
|
let bitcoind = BitcoinD::new(bitcoind_config, wo_path_str)?;
|
||||||
|
bitcoind.node_sanity_checks(
|
||||||
|
config.bitcoin_config.network,
|
||||||
|
config.main_descriptor.is_taproot(),
|
||||||
|
)?;
|
||||||
|
if fresh_data_dir {
|
||||||
|
log::info!("Creating a new watchonly wallet on bitcoind.");
|
||||||
|
bitcoind.create_watchonly_wallet(&config.main_descriptor)?;
|
||||||
|
log::info!("Watchonly wallet created.");
|
||||||
|
} else {
|
||||||
|
#[cfg(windows)]
|
||||||
|
if !cfg!(test) && !wo_path.exists() {
|
||||||
|
return Err(StartupError::NoWatchonlyInDatadir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::info!("Loading our watchonly wallet on bitcoind.");
|
||||||
|
bitcoind.maybe_load_watchonly_wallet()?;
|
||||||
|
bitcoind.wallet_sanity_checks(&config.main_descriptor)?;
|
||||||
|
log::info!("Watchonly wallet loaded on bitcoind and sanity checked.");
|
||||||
|
|
||||||
|
Ok(bitcoind)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an Electrum interface from a client and BDK-based wallet, and do some sanity checks.
|
||||||
|
// If all went well, returns the interface to Electrum.
|
||||||
|
fn setup_electrum(
|
||||||
|
config: &Config,
|
||||||
|
db: sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
|
||||||
|
) -> Result<Electrum, StartupError> {
|
||||||
|
let electrum_config = match config.bitcoin_backend.as_ref() {
|
||||||
|
Some(config::BitcoinBackend::Electrum(electrum_config)) => electrum_config,
|
||||||
|
_ => Err(StartupError::MissingElectrumConfig)?,
|
||||||
|
};
|
||||||
|
// First create the client to communicate with the Electrum server.
|
||||||
|
let client = electrum::client::Client::new(electrum_config)
|
||||||
|
.map_err(|e| StartupError::Electrum(ElectrumError::Client(e)))?;
|
||||||
|
// Then create the BDK-based wallet and populate it with DB data.
|
||||||
|
let mut db_conn = db.connection();
|
||||||
|
let tip = db_conn.chain_tip();
|
||||||
|
let coins: Vec<_> = db_conn
|
||||||
|
.coins(&[], &[])
|
||||||
|
.into_values()
|
||||||
|
.map(|c| crate::bitcoin::Coin {
|
||||||
|
outpoint: c.outpoint,
|
||||||
|
amount: c.amount,
|
||||||
|
derivation_index: c.derivation_index,
|
||||||
|
is_change: c.is_change,
|
||||||
|
is_immature: c.is_immature,
|
||||||
|
block_info: c.block_info.map(|info| crate::bitcoin::BlockInfo {
|
||||||
|
height: info.height,
|
||||||
|
time: info.time,
|
||||||
|
}),
|
||||||
|
spend_txid: c.spend_txid,
|
||||||
|
spend_block: c.spend_block.map(|info| crate::bitcoin::BlockInfo {
|
||||||
|
height: info.height,
|
||||||
|
time: info.time,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let txids = db_conn.list_saved_txids();
|
||||||
|
// This will only return those txs referenced by our coins, which may not be all of `txids`.
|
||||||
|
let txs: Vec<_> = db_conn
|
||||||
|
.list_wallet_transactions(&txids)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(tx, _, _)| tx)
|
||||||
|
.collect();
|
||||||
|
let (receive_index, change_index) = (db_conn.receive_index(), db_conn.change_index());
|
||||||
|
let genesis_hash = {
|
||||||
|
let chain_hash = ChainHash::using_genesis_block(config.bitcoin_config.network);
|
||||||
|
BlockHash::from_byte_array(*chain_hash.as_bytes())
|
||||||
|
};
|
||||||
|
let bdk_wallet = electrum::wallet::BdkWallet::new(
|
||||||
|
&config.main_descriptor,
|
||||||
|
genesis_hash,
|
||||||
|
tip,
|
||||||
|
&coins,
|
||||||
|
&txs,
|
||||||
|
receive_index,
|
||||||
|
change_index,
|
||||||
|
);
|
||||||
|
let full_scan = db_conn.rescan_timestamp().is_some();
|
||||||
|
let electrum = Electrum::new(client, bdk_wallet, full_scan).map_err(StartupError::Electrum)?;
|
||||||
|
electrum
|
||||||
|
.sanity_checks(&genesis_hash)
|
||||||
|
.map_err(StartupError::Electrum)?;
|
||||||
|
Ok(electrum)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DaemonControl {
|
||||||
|
config: Config,
|
||||||
|
bitcoin: sync::Arc<sync::Mutex<dyn BitcoinInterface>>,
|
||||||
|
poller_sender: mpsc::SyncSender<poller::PollerMessage>,
|
||||||
|
// FIXME: Should we require Sync on DatabaseInterface rather than using a Mutex?
|
||||||
|
db: sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
|
||||||
|
secp: secp256k1::Secp256k1<secp256k1::VerifyOnly>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DaemonControl {
|
||||||
|
pub(crate) fn new(
|
||||||
|
config: Config,
|
||||||
|
bitcoin: sync::Arc<sync::Mutex<dyn BitcoinInterface>>,
|
||||||
|
poller_sender: mpsc::SyncSender<poller::PollerMessage>,
|
||||||
|
db: sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
|
||||||
|
secp: secp256k1::Secp256k1<secp256k1::VerifyOnly>,
|
||||||
|
) -> DaemonControl {
|
||||||
|
DaemonControl {
|
||||||
|
config,
|
||||||
|
bitcoin,
|
||||||
|
poller_sender,
|
||||||
|
db,
|
||||||
|
secp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Useful for unit test to directly mess up with the DB
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn db(&self) -> sync::Arc<sync::Mutex<dyn DatabaseInterface>> {
|
||||||
|
self.db.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The handle to a Liana daemon. It might either be the handle for a daemon which exposes a
|
||||||
|
/// JSONRPC server or one which exposes its API through a `DaemonControl`.
|
||||||
|
pub enum DaemonHandle {
|
||||||
|
Controller {
|
||||||
|
poller_sender: mpsc::SyncSender<poller::PollerMessage>,
|
||||||
|
poller_handle: thread::JoinHandle<()>,
|
||||||
|
control: DaemonControl,
|
||||||
|
},
|
||||||
|
Server {
|
||||||
|
poller_sender: mpsc::SyncSender<poller::PollerMessage>,
|
||||||
|
poller_handle: thread::JoinHandle<()>,
|
||||||
|
rpcserver_shutdown: sync::Arc<sync::atomic::AtomicBool>,
|
||||||
|
rpcserver_handle: thread::JoinHandle<Result<(), io::Error>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DaemonHandle {
|
||||||
|
/// This starts the Liana daemon. A user of this interface should regularly poll the `is_alive`
|
||||||
|
/// method to check for internal errors. To shut down the daemon use the `stop` method.
|
||||||
|
///
|
||||||
|
/// The `with_rpc_server` controls whether we should start a JSONRPC server to receive queries
|
||||||
|
/// or instead return a `DaemonControl` object for a caller to access the daemon's API.
|
||||||
|
///
|
||||||
|
/// You may specify a custom Bitcoin interface through the `bitcoin` parameter. If `None`, the
|
||||||
|
/// default Bitcoin interface (`bitcoind` JSONRPC) will be used.
|
||||||
|
/// You may specify a custom Database interface through the `db` parameter. If `None`, the
|
||||||
|
/// default Database interface (SQLite) will be used.
|
||||||
|
pub fn start(
|
||||||
|
config: Config,
|
||||||
|
bitcoin: Option<impl BitcoinInterface + 'static>,
|
||||||
|
db: Option<impl DatabaseInterface + 'static>,
|
||||||
|
with_rpc_server: bool,
|
||||||
|
) -> Result<Self, StartupError> {
|
||||||
|
#[cfg(not(test))]
|
||||||
|
setup_panic_hook();
|
||||||
|
|
||||||
|
let secp = secp256k1::Secp256k1::verification_only();
|
||||||
|
|
||||||
|
// First, check the data directory
|
||||||
|
let mut data_dir = config
|
||||||
|
.data_dir()
|
||||||
|
.ok_or(StartupError::DefaultDataDirNotFound)?;
|
||||||
|
data_dir.push(config.bitcoin_config.network.to_string());
|
||||||
|
let fresh_data_dir = !data_dir.as_path().exists();
|
||||||
|
if fresh_data_dir {
|
||||||
|
create_datadir(&data_dir)?;
|
||||||
|
log::info!("Created a new data directory at '{}'", data_dir.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up the connection to bitcoind (if using it) first as we may need it for the database
|
||||||
|
// migration when setting up SQLite below.
|
||||||
|
let bitcoind = if bitcoin.is_none() {
|
||||||
|
if let Some(config::BitcoinBackend::Bitcoind(_)) = &config.bitcoin_backend {
|
||||||
|
Some(setup_bitcoind(&config, &data_dir, fresh_data_dir)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Then set up the database backend.
|
||||||
|
let db = match db {
|
||||||
|
Some(db) => sync::Arc::from(sync::Mutex::from(db)),
|
||||||
|
None => sync::Arc::from(sync::Mutex::from(setup_sqlite(
|
||||||
|
&config,
|
||||||
|
&data_dir,
|
||||||
|
fresh_data_dir,
|
||||||
|
&secp,
|
||||||
|
&bitcoind,
|
||||||
|
)?)) as sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Finally set up the Bitcoin backend.
|
||||||
|
let bit = match (bitcoin, &config.bitcoin_backend) {
|
||||||
|
(Some(bit), _) => sync::Arc::from(sync::Mutex::from(bit)),
|
||||||
|
(None, Some(config::BitcoinBackend::Bitcoind(..))) => sync::Arc::from(
|
||||||
|
sync::Mutex::from(bitcoind.expect("bitcoind must have been set already")),
|
||||||
|
)
|
||||||
|
as sync::Arc<sync::Mutex<dyn BitcoinInterface>>,
|
||||||
|
(None, Some(config::BitcoinBackend::Electrum(..))) => {
|
||||||
|
sync::Arc::from(sync::Mutex::from(setup_electrum(&config, db.clone())?))
|
||||||
|
}
|
||||||
|
(None, None) => Err(StartupError::MissingBitcoinBackendConfig)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start the poller thread. Keep the thread handle to be able to check if it crashed. Store
|
||||||
|
// an atomic to be able to stop it.
|
||||||
|
let mut bitcoin_poller =
|
||||||
|
poller::Poller::new(bit.clone(), db.clone(), config.main_descriptor.clone());
|
||||||
|
let (poller_sender, poller_receiver) = mpsc::sync_channel(0);
|
||||||
|
let poller_handle = thread::Builder::new()
|
||||||
|
.name("Bitcoin Network poller".to_string())
|
||||||
|
.spawn({
|
||||||
|
let poll_interval = config.bitcoin_config.poll_interval_secs;
|
||||||
|
move || {
|
||||||
|
log::info!("Bitcoin poller started.");
|
||||||
|
bitcoin_poller.poll_forever(poll_interval, poller_receiver);
|
||||||
|
log::info!("Bitcoin poller stopped.");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.expect("Spawning the poller thread must never fail.");
|
||||||
|
|
||||||
|
// Create the API the external world will use to talk to us, either directly through the Rust
|
||||||
|
// structure or through the JSONRPC server we may setup below.
|
||||||
|
let control = DaemonControl::new(config, bit, poller_sender.clone(), db, secp);
|
||||||
|
|
||||||
|
if with_rpc_server {
|
||||||
|
let rpcserver_shutdown = sync::Arc::from(sync::atomic::AtomicBool::from(false));
|
||||||
|
let rpcserver_handle = thread::Builder::new()
|
||||||
|
.name("Bitcoin Network poller".to_string())
|
||||||
|
.spawn({
|
||||||
|
let shutdown = rpcserver_shutdown.clone();
|
||||||
|
move || {
|
||||||
|
let mut rpc_socket = data_dir;
|
||||||
|
rpc_socket.push("lianad_rpc");
|
||||||
|
server::run(&rpc_socket, control, shutdown)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.expect("Spawning the RPC server thread should never fail.");
|
||||||
|
|
||||||
|
return Ok(DaemonHandle::Server {
|
||||||
|
poller_sender,
|
||||||
|
poller_handle,
|
||||||
|
rpcserver_shutdown,
|
||||||
|
rpcserver_handle,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(DaemonHandle::Controller {
|
||||||
|
poller_sender,
|
||||||
|
poller_handle,
|
||||||
|
control,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start the Liana daemon with the default Bitcoin and database interfaces (`bitcoind` RPC
|
||||||
|
/// and SQLite).
|
||||||
|
pub fn start_default(
|
||||||
|
config: Config,
|
||||||
|
with_rpc_server: bool,
|
||||||
|
) -> Result<DaemonHandle, StartupError> {
|
||||||
|
Self::start(
|
||||||
|
config,
|
||||||
|
Option::<BitcoinD>::None,
|
||||||
|
Option::<SqliteDb>::None,
|
||||||
|
with_rpc_server,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether the daemon is still up and running. This needs to be regularly polled to
|
||||||
|
/// check for internal errors. If this returns `false`, collect the error using the `stop`
|
||||||
|
/// method.
|
||||||
|
pub fn is_alive(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Controller {
|
||||||
|
ref poller_handle, ..
|
||||||
|
} => !poller_handle.is_finished(),
|
||||||
|
Self::Server {
|
||||||
|
ref poller_handle,
|
||||||
|
ref rpcserver_handle,
|
||||||
|
..
|
||||||
|
} => !poller_handle.is_finished() && !rpcserver_handle.is_finished(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop the Liana daemon. This returns any error which may have occurred.
|
||||||
|
pub fn stop(self) -> Result<(), Box<dyn error::Error>> {
|
||||||
|
match self {
|
||||||
|
Self::Controller {
|
||||||
|
poller_sender,
|
||||||
|
poller_handle,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
poller_sender
|
||||||
|
.send(poller::PollerMessage::Shutdown)
|
||||||
|
.expect("The other end should never have hung up before this.");
|
||||||
|
poller_handle.join().expect("Poller thread must not panic");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Self::Server {
|
||||||
|
poller_sender,
|
||||||
|
poller_handle,
|
||||||
|
rpcserver_shutdown,
|
||||||
|
rpcserver_handle,
|
||||||
|
} => {
|
||||||
|
poller_sender
|
||||||
|
.send(poller::PollerMessage::Shutdown)
|
||||||
|
.expect("The other end should never have hung up before this.");
|
||||||
|
rpcserver_shutdown.store(true, sync::atomic::Ordering::Relaxed);
|
||||||
|
rpcserver_handle
|
||||||
|
.join()
|
||||||
|
.expect("Poller thread must not panic")?;
|
||||||
|
poller_handle.join().expect("Poller thread must not panic");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, unix))]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::{
|
||||||
|
config::{BitcoinConfig, BitcoindConfig, BitcoindRpcAuth},
|
||||||
|
testutils::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
use liana::descriptors::LianaDescriptor;
|
||||||
|
|
||||||
|
use miniscript::bitcoin;
|
||||||
|
use std::{
|
||||||
|
fs,
|
||||||
|
io::{BufRead, BufReader, Write},
|
||||||
|
net, path,
|
||||||
|
str::FromStr,
|
||||||
|
thread, time,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read all bytes from the socket until the end of a JSON object, good enough approximation.
|
||||||
|
fn read_til_json_end(stream: &mut net::TcpStream) {
|
||||||
|
stream
|
||||||
|
.set_read_timeout(Some(time::Duration::from_secs(5)))
|
||||||
|
.unwrap();
|
||||||
|
let mut reader = BufReader::new(stream);
|
||||||
|
loop {
|
||||||
|
let mut line = String::new();
|
||||||
|
reader.read_line(&mut line).unwrap();
|
||||||
|
|
||||||
|
if line.starts_with("Authorization") {
|
||||||
|
let mut buf = vec![0; 256];
|
||||||
|
reader.read_until(b'}', &mut buf).unwrap();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Respond to the two "echo" sent at startup to sanity check the connection
|
||||||
|
fn complete_sanity_check(server: &net::TcpListener) {
|
||||||
|
let echo_resp =
|
||||||
|
"HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[]}\n".as_bytes();
|
||||||
|
|
||||||
|
// Read the first echo, respond to it
|
||||||
|
{
|
||||||
|
let (mut stream, _) = server.accept().unwrap();
|
||||||
|
read_til_json_end(&mut stream);
|
||||||
|
stream.write_all(echo_resp).unwrap();
|
||||||
|
stream.flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the second echo, respond to it
|
||||||
|
let (mut stream, _) = server.accept().unwrap();
|
||||||
|
read_til_json_end(&mut stream);
|
||||||
|
stream.write_all(echo_resp).unwrap();
|
||||||
|
stream.flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send them a pruned getblockchaininfo telling them we are at version 24.0
|
||||||
|
fn complete_version_check(server: &net::TcpListener) {
|
||||||
|
let net_resp =
|
||||||
|
"HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"version\":240000}}\n"
|
||||||
|
.as_bytes();
|
||||||
|
let (mut stream, _) = server.accept().unwrap();
|
||||||
|
read_til_json_end(&mut stream);
|
||||||
|
stream.write_all(net_resp).unwrap();
|
||||||
|
stream.flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send them a pruned getblockchaininfo telling them we are on mainnet
|
||||||
|
fn complete_network_check(server: &net::TcpListener) {
|
||||||
|
let net_resp =
|
||||||
|
"HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"chain\":\"main\"}}\n"
|
||||||
|
.as_bytes();
|
||||||
|
let (mut stream, _) = server.accept().unwrap();
|
||||||
|
read_til_json_end(&mut stream);
|
||||||
|
stream.write_all(net_resp).unwrap();
|
||||||
|
stream.flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send them responses for the calls involved when creating a fresh wallet
|
||||||
|
fn complete_wallet_creation(server: &net::TcpListener) {
|
||||||
|
{
|
||||||
|
let net_resp =
|
||||||
|
["HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[]}\n".as_bytes()]
|
||||||
|
.concat();
|
||||||
|
let (mut stream, _) = server.accept().unwrap();
|
||||||
|
read_til_json_end(&mut stream);
|
||||||
|
stream.write_all(&net_resp).unwrap();
|
||||||
|
stream.flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let net_resp = [
|
||||||
|
"HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"name\":\"dummy\"}}\n"
|
||||||
|
.as_bytes(),
|
||||||
|
]
|
||||||
|
.concat();
|
||||||
|
let (mut stream, _) = server.accept().unwrap();
|
||||||
|
read_til_json_end(&mut stream);
|
||||||
|
stream.write_all(&net_resp).unwrap();
|
||||||
|
stream.flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let net_resp = [
|
||||||
|
"HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[{\"success\":true}]}\n"
|
||||||
|
.as_bytes(),
|
||||||
|
]
|
||||||
|
.concat();
|
||||||
|
let (mut stream, _) = server.accept().unwrap();
|
||||||
|
read_til_json_end(&mut stream);
|
||||||
|
stream.write_all(&net_resp).unwrap();
|
||||||
|
stream.flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send them a dummy result to loadwallet.
|
||||||
|
fn complete_wallet_loading(server: &net::TcpListener) {
|
||||||
|
{
|
||||||
|
let listwallets_resp =
|
||||||
|
"HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[]}\n".as_bytes();
|
||||||
|
let (mut stream, _) = server.accept().unwrap();
|
||||||
|
read_til_json_end(&mut stream);
|
||||||
|
stream.write_all(listwallets_resp).unwrap();
|
||||||
|
stream.flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let loadwallet_resp =
|
||||||
|
"HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"name\":\"dummy\"}}\n"
|
||||||
|
.as_bytes();
|
||||||
|
let (mut stream, _) = server.accept().unwrap();
|
||||||
|
read_til_json_end(&mut stream);
|
||||||
|
stream.write_all(loadwallet_resp).unwrap();
|
||||||
|
stream.flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send them a response to 'listwallets' with the watchonly wallet path
|
||||||
|
fn complete_wallet_check(server: &net::TcpListener, watchonly_wallet_path: &str) {
|
||||||
|
let net_resp = [
|
||||||
|
"HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[\"".as_bytes(),
|
||||||
|
watchonly_wallet_path.as_bytes(),
|
||||||
|
"\"]}\n".as_bytes(),
|
||||||
|
]
|
||||||
|
.concat();
|
||||||
|
let (mut stream, _) = server.accept().unwrap();
|
||||||
|
read_til_json_end(&mut stream);
|
||||||
|
stream.write_all(&net_resp).unwrap();
|
||||||
|
stream.flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send them a response to 'listdescriptors' with the receive and change descriptors
|
||||||
|
fn complete_desc_check(server: &net::TcpListener, receive_desc: &str, change_desc: &str) {
|
||||||
|
let net_resp = [
|
||||||
|
"HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"descriptors\":[{\"desc\":\"".as_bytes(),
|
||||||
|
receive_desc.as_bytes(),
|
||||||
|
"\",\"timestamp\":0},".as_bytes(),
|
||||||
|
"{\"desc\":\"".as_bytes(),
|
||||||
|
change_desc.as_bytes(),
|
||||||
|
"\",\"timestamp\":1}]}}\n".as_bytes(),
|
||||||
|
]
|
||||||
|
.concat();
|
||||||
|
let (mut stream, _) = server.accept().unwrap();
|
||||||
|
read_til_json_end(&mut stream);
|
||||||
|
stream.write_all(&net_resp).unwrap();
|
||||||
|
stream.flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send them a response to 'getblockhash' with the genesis block hash
|
||||||
|
fn complete_tip_init(server: &net::TcpListener) {
|
||||||
|
let net_resp = [
|
||||||
|
"HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f\"}\n".as_bytes(),
|
||||||
|
]
|
||||||
|
.concat();
|
||||||
|
let (mut stream, _) = server.accept().unwrap();
|
||||||
|
read_til_json_end(&mut stream);
|
||||||
|
stream.write_all(&net_resp).unwrap();
|
||||||
|
stream.flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: we could move the dummy bitcoind thread stuff to the bitcoind module to test the
|
||||||
|
// bitcoind interface, and use the DummyLiana from testutils to sanity check the startup.
|
||||||
|
// Note that startup as checked by this unit test is also tested in the functional test
|
||||||
|
// framework.
|
||||||
|
#[test]
|
||||||
|
fn daemon_startup() {
|
||||||
|
let tmp_dir = tmp_dir();
|
||||||
|
fs::create_dir_all(&tmp_dir).unwrap();
|
||||||
|
let data_dir: path::PathBuf = [tmp_dir.as_path(), path::Path::new("datadir")]
|
||||||
|
.iter()
|
||||||
|
.collect();
|
||||||
|
let wo_path: path::PathBuf = [
|
||||||
|
data_dir.as_path(),
|
||||||
|
path::Path::new("bitcoin"),
|
||||||
|
path::Path::new("lianad_watchonly_wallet"),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.collect();
|
||||||
|
let wo_path = wo_path.to_str().unwrap().to_string();
|
||||||
|
|
||||||
|
// Configure a dummy bitcoind
|
||||||
|
let network = bitcoin::Network::Bitcoin;
|
||||||
|
let cookie: path::PathBuf = [
|
||||||
|
tmp_dir.as_path(),
|
||||||
|
path::Path::new(&format!(
|
||||||
|
"dummy_bitcoind_{:?}.cookie",
|
||||||
|
thread::current().id()
|
||||||
|
)),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.collect();
|
||||||
|
fs::write(&cookie, [0; 32]).unwrap(); // Will overwrite should it exist already
|
||||||
|
let addr: net::SocketAddr =
|
||||||
|
net::SocketAddrV4::new(net::Ipv4Addr::new(127, 0, 0, 1), 0).into();
|
||||||
|
let server = net::TcpListener::bind(addr).unwrap();
|
||||||
|
let addr = server.local_addr().unwrap();
|
||||||
|
let bitcoin_config = BitcoinConfig {
|
||||||
|
network,
|
||||||
|
poll_interval_secs: time::Duration::from_secs(2),
|
||||||
|
};
|
||||||
|
let bitcoind_config = BitcoindConfig {
|
||||||
|
addr,
|
||||||
|
rpc_auth: BitcoindRpcAuth::CookieFile(cookie),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a dummy config with this bitcoind
|
||||||
|
let desc_str = "wsh(andor(pk([aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/<0;1>/*),older(10000),pk([aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/<0;1>/*)))#3xh8xmhn";
|
||||||
|
let desc = LianaDescriptor::from_str(desc_str).unwrap();
|
||||||
|
let receive_desc = desc.receive_descriptor().clone();
|
||||||
|
let change_desc = desc.change_descriptor().clone();
|
||||||
|
let config = Config {
|
||||||
|
bitcoin_config,
|
||||||
|
bitcoin_backend: Some(config::BitcoinBackend::Bitcoind(bitcoind_config)),
|
||||||
|
data_dir: Some(data_dir),
|
||||||
|
log_level: log::LevelFilter::Debug,
|
||||||
|
main_descriptor: desc,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start the daemon in a new thread so the current one acts as the bitcoind server.
|
||||||
|
let t = thread::spawn({
|
||||||
|
let config = config.clone();
|
||||||
|
move || {
|
||||||
|
let handle = DaemonHandle::start_default(config, false).unwrap();
|
||||||
|
handle.stop().unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
complete_sanity_check(&server);
|
||||||
|
complete_version_check(&server);
|
||||||
|
complete_network_check(&server);
|
||||||
|
complete_wallet_creation(&server);
|
||||||
|
complete_wallet_loading(&server);
|
||||||
|
complete_wallet_check(&server, &wo_path);
|
||||||
|
complete_desc_check(&server, &receive_desc.to_string(), &change_desc.to_string());
|
||||||
|
complete_tip_init(&server);
|
||||||
|
// We don't have to complete the sync check as the poller checks whether it needs to stop
|
||||||
|
// before checking the bitcoind sync status.
|
||||||
|
t.join().unwrap();
|
||||||
|
|
||||||
|
// The datadir is created now, so if we restart it it won't create the wo wallet.
|
||||||
|
let t = thread::spawn({
|
||||||
|
let config = config.clone();
|
||||||
|
move || {
|
||||||
|
let handle = DaemonHandle::start_default(config, false).unwrap();
|
||||||
|
handle.stop().unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
complete_sanity_check(&server);
|
||||||
|
complete_version_check(&server);
|
||||||
|
complete_network_check(&server);
|
||||||
|
complete_wallet_loading(&server);
|
||||||
|
complete_wallet_check(&server, &wo_path);
|
||||||
|
complete_desc_check(&server, &receive_desc.to_string(), &change_desc.to_string());
|
||||||
|
// We don't have to complete the sync check as the poller checks whether it needs to stop
|
||||||
|
// before checking the bitcoind sync status.
|
||||||
|
t.join().unwrap();
|
||||||
|
|
||||||
|
fs::remove_dir_all(&tmp_dir).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,8 +4,9 @@ use crate::{
|
|||||||
database::{
|
database::{
|
||||||
BlockInfo, Coin, CoinStatus, DatabaseConnection, DatabaseInterface, LabelItem, Wallet,
|
BlockInfo, Coin, CoinStatus, DatabaseConnection, DatabaseInterface, LabelItem, Wallet,
|
||||||
},
|
},
|
||||||
descriptors, DaemonControl, DaemonHandle,
|
DaemonControl, DaemonHandle,
|
||||||
};
|
};
|
||||||
|
use liana::descriptors;
|
||||||
|
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::{
|
use std::{
|
||||||
Loading…
x
Reference in New Issue
Block a user