Merge #1476: Split liana into liana and lianad crates
da4c102a66c1f340797be13d70659b2f875c3438 Fix reproducible system to handle lianad crate (edouardparis)
74820d97e9257e44c5c9d3bc64926e30066b285a Split into lianad and liana crate (edouardparis)
Pull request description:
This change introduce two crates:
- `lianad` the crate responsible to create the `lianad` and `liana-cli` binaries and expose daemon modules for `liana-gui` to embed
- `liana` the crate responsible for the descriptors and the core logic of the bitcoin scripts and spend transactions creation. This will allow multiple platforms and mediums to start manipulating these modules without importing all the daemon dependencies (like the database or bitcoin core clients).
ACKs for top commit:
jp1ac4:
ACK da4c102a66.
Tree-SHA512: 85a268b2bffe5fe7b54d7b1e452dd2097ae606b903e5684a7b0e996b61981ab222b6718fb19739a05d1153b4d48a85d0ec3e1e15f68afe17936eec80111ecf04
This commit is contained in:
commit
1f7c41ae5b
@ -75,7 +75,7 @@ task:
|
||||
fingerprint_script:
|
||||
- rustc --version
|
||||
- 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
|
||||
|
||||
|
||||
27
Cargo.lock
generated
27
Cargo.lock
generated
@ -2804,21 +2804,13 @@ dependencies = [
|
||||
name = "liana"
|
||||
version = "8.0.0"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bdk_coin_select",
|
||||
"bdk_electrum",
|
||||
"bip39",
|
||||
"dirs 5.0.1",
|
||||
"fern",
|
||||
"getrandom",
|
||||
"jsonrpc 0.17.0",
|
||||
"log",
|
||||
"miniscript",
|
||||
"rdrand",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2850,6 +2842,7 @@ dependencies = [
|
||||
"jsonrpc 0.12.1",
|
||||
"liana",
|
||||
"liana-ui",
|
||||
"lianad",
|
||||
"log",
|
||||
"reqwest",
|
||||
"rust-ini",
|
||||
@ -2872,6 +2865,24 @@ dependencies = [
|
||||
"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]]
|
||||
name = "libc"
|
||||
version = "0.2.162"
|
||||
|
||||
@ -3,10 +3,11 @@ resolver = "2"
|
||||
members = [
|
||||
"fuzz",
|
||||
"liana",
|
||||
"lianad",
|
||||
"liana-gui",
|
||||
"liana-ui",
|
||||
]
|
||||
default-members = ["liana", "liana-gui", "liana-ui"]
|
||||
default-members = ["liana", "lianad", "liana-gui", "liana-ui"]
|
||||
|
||||
[patch.crates-io]
|
||||
iced_style = { git = "https://github.com/edouardparis/iced", branch = "patch-0.12.3"}
|
||||
|
||||
@ -17,6 +17,8 @@ docker run --rm -ti \
|
||||
-v "$PWD/Cargo.lock":/liana/Cargo.lock \
|
||||
-v "$PWD/liana/Cargo.toml":/liana/liana/Cargo.toml \
|
||||
-v "$PWD/liana/src":/liana/liana/src \
|
||||
-v "$PWD/lianad/Cargo.toml":/liana/lianad/Cargo.toml \
|
||||
-v "$PWD/lianad/src":/liana/lianad/src \
|
||||
-v "$PWD/liana-gui/Cargo.toml":/liana/liana-gui/Cargo.toml \
|
||||
-v "$PWD/liana-gui/src":/liana/liana-gui/src \
|
||||
-v "$PWD/liana-ui/Cargo.toml":/liana/liana-ui/Cargo.toml \
|
||||
@ -41,6 +43,8 @@ docker run -ti \
|
||||
-v "$PWD/Cargo.lock":/liana/Cargo.lock \
|
||||
-v "$PWD/liana/Cargo.toml":/liana/liana/Cargo.toml \
|
||||
-v "$PWD/liana/src":/liana/liana/src \
|
||||
-v "$PWD/lianad/Cargo.toml":/liana/lianad/Cargo.toml \
|
||||
-v "$PWD/lianad/src":/liana/lianad/src \
|
||||
-v "$PWD/liana-gui/Cargo.toml":/liana/liana-gui/Cargo.toml \
|
||||
-v "$PWD/liana-gui/src":/liana/liana-gui/src \
|
||||
-v "$PWD/liana-ui/Cargo.toml":/liana/liana-ui/Cargo.toml \
|
||||
|
||||
@ -29,7 +29,7 @@ cd ..
|
||||
# Finally build the projects using the toolchain just created.
|
||||
alias cargo="/liana/rust-1.71.1-x86_64-unknown-linux-gnu/cargo/bin/cargo"
|
||||
|
||||
for package_name in "liana" "liana-gui"; do
|
||||
for package_name in "lianad" "liana-gui"; do
|
||||
PATH="$PATH:$PWD/osxcross/target/bin/" \
|
||||
CC=o64-clang \
|
||||
CXX=o64-clang++ \
|
||||
|
||||
@ -25,7 +25,7 @@ export CARGO_HOME="/liana/.cargo"
|
||||
|
||||
# We need to set RUSTC_BOOTSTRAP=1 as a workaround to be able to use unstable
|
||||
# features in the GUI dependencies
|
||||
for package_name in "liana" "liana-gui"; do
|
||||
for package_name in "lianad" "liana-gui"; do
|
||||
RUSTC_BOOTSTRAP=1 cargo -vvv \
|
||||
--color always \
|
||||
--frozen \
|
||||
|
||||
@ -80,6 +80,8 @@ time_machine shell --no-cwd \
|
||||
--expose="$BUILD_ROOT/Cargo.lock=/liana/Cargo.lock" \
|
||||
--expose="$PWD/liana/src=/liana/liana/src" \
|
||||
--expose="$PWD/liana/Cargo.toml=/liana/liana/Cargo.toml" \
|
||||
--expose="$PWD/lianad/src=/liana/lianad/src" \
|
||||
--expose="$PWD/lianad/Cargo.toml=/liana/lianad/Cargo.toml" \
|
||||
--expose="$PWD/liana-gui/Cargo.toml=/liana/liana-gui/Cargo.toml" \
|
||||
--expose="$PWD/liana-gui/src=/liana/liana-gui/src" \
|
||||
--expose="$PWD/liana-ui/src=/liana/liana-ui/src" \
|
||||
|
||||
@ -16,7 +16,8 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
async-trait = "0.1"
|
||||
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" }
|
||||
backtrace = "0.3"
|
||||
hex = "0.4.3"
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
use std::convert::From;
|
||||
use std::io::ErrorKind;
|
||||
|
||||
use liana::{config::ConfigError, descriptors::LianaDescError, spend::SpendCreationError};
|
||||
use liana::{descriptors::LianaDescError, spend::SpendCreationError};
|
||||
use lianad::config::ConfigError;
|
||||
|
||||
use crate::{
|
||||
app::{settings::SettingsError, wallet::WalletError},
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use liana::{
|
||||
config::Config as DaemonConfig,
|
||||
miniscript::bitcoin::{
|
||||
bip32::{ChildNumber, Fingerprint},
|
||||
psbt::Psbt,
|
||||
Address, Txid,
|
||||
},
|
||||
use liana::miniscript::bitcoin::{
|
||||
bip32::{ChildNumber, Fingerprint},
|
||||
psbt::Psbt,
|
||||
Address, Txid,
|
||||
};
|
||||
use lianad::config::Config as DaemonConfig;
|
||||
|
||||
use crate::{
|
||||
app::{cache::Cache, error::Error, view, wallet::Wallet},
|
||||
|
||||
@ -19,11 +19,12 @@ use iced::{clipboard, time, Command, Subscription};
|
||||
use tokio::runtime::Handle;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
pub use liana::{commands::CoinStatus, config::Config as DaemonConfig, miniscript::bitcoin};
|
||||
pub use liana::miniscript::bitcoin;
|
||||
use liana_ui::{
|
||||
component::network_banner,
|
||||
widget::{Column, Element},
|
||||
};
|
||||
pub use lianad::{commands::CoinStatus, config::Config as DaemonConfig};
|
||||
|
||||
pub use config::Config;
|
||||
pub use message::Message;
|
||||
|
||||
@ -4,8 +4,8 @@ use std::{cmp::Ordering, collections::HashSet};
|
||||
|
||||
use iced::Command;
|
||||
|
||||
use liana::commands::CoinStatus;
|
||||
use liana_ui::widget::Element;
|
||||
use lianad::commands::CoinStatus;
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
|
||||
@ -13,11 +13,9 @@ use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use iced::{Command, Subscription};
|
||||
use liana::{
|
||||
commands::CoinStatus,
|
||||
miniscript::bitcoin::{Amount, OutPoint},
|
||||
};
|
||||
use liana::miniscript::bitcoin::{Amount, OutPoint};
|
||||
use liana_ui::widget::*;
|
||||
use lianad::commands::CoinStatus;
|
||||
|
||||
use super::{
|
||||
cache::Cache,
|
||||
|
||||
@ -7,10 +7,10 @@ use iced::Subscription;
|
||||
|
||||
use iced::Command;
|
||||
use liana::{
|
||||
commands::CoinStatus,
|
||||
descriptors::LianaPolicy,
|
||||
miniscript::bitcoin::{bip32::Fingerprint, psbt::Psbt, Network, Txid},
|
||||
};
|
||||
use lianad::commands::CoinStatus;
|
||||
|
||||
use liana_ui::component::toast;
|
||||
use liana_ui::{
|
||||
|
||||
@ -4,14 +4,12 @@ use std::sync::Arc;
|
||||
|
||||
use iced::Command;
|
||||
|
||||
use liana::{
|
||||
commands::CoinStatus,
|
||||
miniscript::bitcoin::{
|
||||
bip32::{DerivationPath, Fingerprint},
|
||||
secp256k1,
|
||||
},
|
||||
use liana::miniscript::bitcoin::{
|
||||
bip32::{DerivationPath, Fingerprint},
|
||||
secp256k1,
|
||||
};
|
||||
use liana_ui::{component::form, widget::Element};
|
||||
use lianad::commands::CoinStatus;
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
|
||||
@ -8,11 +8,9 @@ use chrono::{NaiveDate, Utc};
|
||||
use iced::Command;
|
||||
use tracing::info;
|
||||
|
||||
use liana::{
|
||||
config::{
|
||||
BitcoinBackend, BitcoinConfig, BitcoindConfig, BitcoindRpcAuth, Config, ElectrumConfig,
|
||||
},
|
||||
miniscript::bitcoin::Network,
|
||||
use liana::miniscript::bitcoin::Network;
|
||||
use lianad::config::{
|
||||
BitcoinBackend, BitcoinConfig, BitcoindConfig, BitcoindRpcAuth, Config, ElectrumConfig,
|
||||
};
|
||||
|
||||
use liana_ui::{component::form, widget::Element};
|
||||
@ -353,7 +351,7 @@ impl BitcoindSettings {
|
||||
if let (true, Some(rpc_auth)) = (self.addr.valid, rpc_auth) {
|
||||
let mut daemon_config = daemon.config().cloned().unwrap();
|
||||
daemon_config.bitcoin_backend =
|
||||
Some(liana::config::BitcoinBackend::Bitcoind(BitcoindConfig {
|
||||
Some(lianad::config::BitcoinBackend::Bitcoind(BitcoindConfig {
|
||||
rpc_auth,
|
||||
addr: new_addr.unwrap(),
|
||||
}));
|
||||
@ -461,7 +459,7 @@ impl ElectrumSettings {
|
||||
if self.addr.valid {
|
||||
let mut daemon_config = daemon.config().cloned().unwrap();
|
||||
daemon_config.bitcoin_backend =
|
||||
Some(liana::config::BitcoinBackend::Electrum(ElectrumConfig {
|
||||
Some(lianad::config::BitcoinBackend::Electrum(ElectrumConfig {
|
||||
addr: self.addr.value.clone(),
|
||||
}));
|
||||
self.processing = true;
|
||||
|
||||
@ -5,11 +5,9 @@ use std::sync::Arc;
|
||||
|
||||
use iced::Command;
|
||||
|
||||
use liana::{
|
||||
commands::CoinStatus,
|
||||
miniscript::bitcoin::{Network, OutPoint},
|
||||
};
|
||||
use liana::miniscript::bitcoin::{Network, OutPoint};
|
||||
use liana_ui::widget::Element;
|
||||
use lianad::commands::CoinStatus;
|
||||
|
||||
use super::{redirect, State};
|
||||
use crate::{
|
||||
|
||||
@ -8,13 +8,13 @@ use std::{
|
||||
|
||||
use iced::{Command, Subscription};
|
||||
use liana::{
|
||||
commands::ListCoinsEntry,
|
||||
descriptors::LianaDescriptor,
|
||||
miniscript::bitcoin::{
|
||||
address, psbt::Psbt, secp256k1, Address, Amount, Denomination, Network, OutPoint,
|
||||
},
|
||||
spend::{SpendCreationError, MAX_FEERATE},
|
||||
};
|
||||
use lianad::commands::ListCoinsEntry;
|
||||
|
||||
use liana_ui::{component::form, widget::Element};
|
||||
|
||||
|
||||
@ -7,7 +7,6 @@ use std::{
|
||||
|
||||
use iced::Command;
|
||||
use liana::{
|
||||
commands::CoinStatus,
|
||||
miniscript::bitcoin::{OutPoint, Txid},
|
||||
spend::{SpendCreationError, MAX_FEERATE},
|
||||
};
|
||||
@ -15,6 +14,7 @@ use liana_ui::{
|
||||
component::{form, modal::Modal},
|
||||
widget::*,
|
||||
};
|
||||
use lianad::commands::CoinStatus;
|
||||
|
||||
pub const HISTORY_EVENT_PAGE_SIZE: u64 = 20;
|
||||
|
||||
|
||||
@ -8,10 +8,10 @@ use iced::{
|
||||
};
|
||||
|
||||
use liana::{
|
||||
config::BitcoindRpcAuth,
|
||||
descriptors::{LianaDescriptor, LianaPolicy},
|
||||
miniscript::bitcoin::{bip32::Fingerprint, Network},
|
||||
};
|
||||
use lianad::config::BitcoindRpcAuth;
|
||||
|
||||
use super::{dashboard, message::*};
|
||||
|
||||
@ -452,7 +452,7 @@ pub fn bitcoind_edit<'a>(
|
||||
pub fn bitcoind<'a>(
|
||||
is_configured_node_type: bool,
|
||||
network: Network,
|
||||
config: &liana::config::BitcoindConfig,
|
||||
config: &lianad::config::BitcoindConfig,
|
||||
blockheight: i32,
|
||||
is_running: Option<bool>,
|
||||
can_edit: bool,
|
||||
@ -638,7 +638,7 @@ pub fn electrum_edit<'a>(
|
||||
pub fn electrum<'a>(
|
||||
is_configured_node_type: bool,
|
||||
network: Network,
|
||||
config: &liana::config::ElectrumConfig,
|
||||
config: &lianad::config::ElectrumConfig,
|
||||
blockheight: i32,
|
||||
is_running: Option<bool>,
|
||||
can_edit: bool,
|
||||
|
||||
@ -4,7 +4,6 @@ use std::iter::FromIterator;
|
||||
use std::path::Path;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use liana::commands::{CoinStatus, CreateRecoveryResult};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
@ -13,10 +12,10 @@ use tracing::{error, info};
|
||||
pub mod error;
|
||||
pub mod jsonrpc;
|
||||
|
||||
use liana::{
|
||||
commands::LabelItem,
|
||||
use liana::miniscript::bitcoin::{address, psbt::Psbt, Address, Network, OutPoint, Txid};
|
||||
use lianad::{
|
||||
commands::{CoinStatus, CreateRecoveryResult, LabelItem},
|
||||
config::Config,
|
||||
miniscript::bitcoin::{address, psbt::Psbt, Address, Network, OutPoint, Txid},
|
||||
};
|
||||
|
||||
use super::{model::*, Daemon, DaemonBackend, DaemonError};
|
||||
|
||||
@ -4,10 +4,10 @@ use tokio::sync::Mutex;
|
||||
|
||||
use super::{model::*, node, Daemon, DaemonBackend, DaemonError};
|
||||
use async_trait::async_trait;
|
||||
use liana::{
|
||||
use liana::miniscript::bitcoin::{address, psbt::Psbt, Address, Network, OutPoint, Txid};
|
||||
use lianad::{
|
||||
commands::{CoinStatus, LabelItem},
|
||||
config::Config,
|
||||
miniscript::bitcoin::{address, psbt::Psbt, Address, Network, OutPoint, Txid},
|
||||
DaemonControl, DaemonHandle,
|
||||
};
|
||||
|
||||
|
||||
@ -11,12 +11,12 @@ use std::path::Path;
|
||||
|
||||
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},
|
||||
config::Config,
|
||||
miniscript::bitcoin::{
|
||||
address, bip32::Fingerprint, psbt::Psbt, secp256k1, Address, Network, OutPoint, Txid,
|
||||
},
|
||||
StartupError,
|
||||
};
|
||||
|
||||
|
||||
@ -3,11 +3,6 @@ use std::collections::{HashMap, HashSet};
|
||||
|
||||
use liana::descriptors::LianaDescriptor;
|
||||
pub use liana::{
|
||||
commands::{
|
||||
CreateSpendResult, GetAddressResult, GetInfoResult, GetLabelsResult, LabelItem,
|
||||
ListCoinsEntry, ListCoinsResult, ListSpendEntry, ListSpendResult, ListTransactionsResult,
|
||||
TransactionInfo,
|
||||
},
|
||||
descriptors::{LianaPolicy, PartialSpendInfo, PathSpendInfo},
|
||||
miniscript::bitcoin::{
|
||||
bip32::{DerivationPath, Fingerprint},
|
||||
@ -15,6 +10,10 @@ pub use liana::{
|
||||
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;
|
||||
|
||||
|
||||
@ -9,11 +9,8 @@ use crate::{
|
||||
signer::Signer,
|
||||
};
|
||||
use async_hwi::DeviceKind;
|
||||
use liana::{
|
||||
config::{BitcoinBackend, BitcoinConfig},
|
||||
descriptors::LianaDescriptor,
|
||||
miniscript::bitcoin,
|
||||
};
|
||||
use liana::{descriptors::LianaDescriptor, miniscript::bitcoin};
|
||||
use lianad::config::{BitcoinBackend, BitcoinConfig};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RemoteBackend {
|
||||
|
||||
@ -5,14 +5,12 @@ mod step;
|
||||
mod view;
|
||||
|
||||
use iced::{clipboard, Command, Subscription};
|
||||
use liana::{
|
||||
config::Config,
|
||||
miniscript::bitcoin::{self, Network},
|
||||
};
|
||||
use liana::miniscript::bitcoin::{self, Network};
|
||||
use liana_ui::{
|
||||
component::network_banner,
|
||||
widget::{Column, Element},
|
||||
};
|
||||
use lianad::config::Config;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
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
|
||||
match liana::DaemonHandle::start_default(cfg, false) {
|
||||
match lianad::DaemonHandle::start_default(cfg, false) {
|
||||
Ok(daemon) => daemon
|
||||
.stop()
|
||||
.map_err(|e| Error::Unexpected(format!("Failed to stop Liana daemon: {}", e))),
|
||||
@ -349,7 +347,7 @@ pub async fn install_local_wallet(
|
||||
ctx: Context,
|
||||
signer: Arc<Mutex<Signer>>,
|
||||
) -> 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 = data_dir
|
||||
|
||||
@ -8,10 +8,8 @@ use bitcoin_hashes::{sha256, Hash};
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
use flate2::read::GzDecoder;
|
||||
use iced::{Command, Subscription};
|
||||
use liana::{
|
||||
config::{BitcoinBackend, BitcoindConfig, BitcoindRpcAuth},
|
||||
miniscript::bitcoin::Network,
|
||||
};
|
||||
use liana::miniscript::bitcoin::Network;
|
||||
use lianad::config::{BitcoinBackend, BitcoindConfig, BitcoindRpcAuth};
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
use tar::Archive;
|
||||
use tracing::info;
|
||||
@ -473,7 +471,7 @@ impl DefineBitcoind {
|
||||
}
|
||||
(Some(rpc_auth), Ok(addr)) => {
|
||||
ctx.bitcoin_backend =
|
||||
Some(liana::config::BitcoinBackend::Bitcoind(BitcoindConfig {
|
||||
Some(lianad::config::BitcoinBackend::Bitcoind(BitcoindConfig {
|
||||
rpc_auth,
|
||||
addr,
|
||||
}));
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
use iced::Command;
|
||||
use liana::{
|
||||
use liana_ui::{component::form, widget::*};
|
||||
use lianad::{
|
||||
config::ElectrumConfig,
|
||||
electrum_client::{self, ElectrumApi},
|
||||
};
|
||||
use liana_ui::{component::form, widget::*};
|
||||
|
||||
use crate::{
|
||||
installer::{
|
||||
@ -45,7 +45,7 @@ impl DefineElectrum {
|
||||
|
||||
pub fn apply(&mut self, ctx: &mut Context) -> bool {
|
||||
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(),
|
||||
}));
|
||||
return true;
|
||||
|
||||
@ -6,13 +6,14 @@ use iced::{
|
||||
Alignment, Command, Length, Subscription,
|
||||
};
|
||||
|
||||
use liana::{config::ConfigError, miniscript::bitcoin::Network};
|
||||
use liana::miniscript::bitcoin::Network;
|
||||
use liana_ui::{
|
||||
color,
|
||||
component::{button, card, modal::Modal, network_banner, notification, text::*},
|
||||
icon, image, theme,
|
||||
widget::*,
|
||||
};
|
||||
use lianad::config::ConfigError;
|
||||
|
||||
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 {
|
||||
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::DatadirNotFound => {
|
||||
format!(
|
||||
|
||||
@ -9,11 +9,13 @@ use std::{
|
||||
use async_trait::async_trait;
|
||||
use chrono::Utc;
|
||||
use liana::{
|
||||
commands::{CoinStatus, GetInfoDescriptors, LCSpendInfo, LabelItem},
|
||||
config::Config,
|
||||
descriptors::LianaDescriptor,
|
||||
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 tokio::sync::RwLock;
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ pub mod node;
|
||||
pub mod signer;
|
||||
pub mod utils;
|
||||
|
||||
use liana::Version;
|
||||
use lianad::Version;
|
||||
|
||||
pub const VERSION: Version = Version {
|
||||
major: 8,
|
||||
|
||||
@ -9,18 +9,18 @@ use iced::{Alignment, Command, Length, Subscription};
|
||||
use tokio::runtime::Handle;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use liana::{
|
||||
commands::CoinStatus,
|
||||
config::{BitcoinBackend, Config, ConfigError},
|
||||
miniscript::bitcoin,
|
||||
StartupError,
|
||||
};
|
||||
use liana::miniscript::bitcoin;
|
||||
use liana_ui::{
|
||||
color,
|
||||
component::{button, notification, text::*},
|
||||
icon,
|
||||
widget::*,
|
||||
};
|
||||
use lianad::{
|
||||
commands::CoinStatus,
|
||||
config::{BitcoinBackend, Config, ConfigError},
|
||||
StartupError,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
@ -533,7 +533,7 @@ pub async fn start_bitcoind_and_daemon(
|
||||
if start_internal_bitcoind {
|
||||
if let Some(BitcoinBackend::Bitcoind(bitcoind_config)) = &config.bitcoin_backend {
|
||||
// Check if bitcoind is already running before trying to start it.
|
||||
if liana::BitcoinD::new(bitcoind_config, "internal_bitcoind_start".to_string()).is_ok()
|
||||
if lianad::BitcoinD::new(bitcoind_config, "internal_bitcoind_start".to_string()).is_ok()
|
||||
{
|
||||
info!("Internal bitcoind is already running");
|
||||
} else {
|
||||
|
||||
@ -17,8 +17,9 @@ use tracing_subscriber::filter::LevelFilter;
|
||||
extern crate serde;
|
||||
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 lianad::config::Config as DaemonConfig;
|
||||
|
||||
use liana_gui::{
|
||||
app::{self, cache::Cache, config::default_datadir, wallet::Wallet, App},
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
use base64::Engine;
|
||||
use bitcoin_hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine};
|
||||
use liana::{
|
||||
config::BitcoindConfig,
|
||||
miniscript::bitcoin::{self, Network},
|
||||
random::{random_bytes, RandomnessError},
|
||||
};
|
||||
use liana_ui::component::form;
|
||||
use lianad::config::BitcoindConfig;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
@ -462,7 +462,7 @@ impl Bitcoind {
|
||||
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(_) => {
|
||||
log::info!("Bitcoind seems to have successfully started.");
|
||||
return Ok(Self {
|
||||
@ -470,7 +470,7 @@ impl Bitcoind {
|
||||
_process: Arc::new(process),
|
||||
});
|
||||
}
|
||||
Err(liana::BitcoindError::CookieFile(_)) => {
|
||||
Err(lianad::BitcoindError::CookieFile(_)) => {
|
||||
// This is only raised if we're using cookie authentication.
|
||||
// Assume cookie file has not been created yet and try again.
|
||||
}
|
||||
@ -497,7 +497,7 @@ impl Bitcoind {
|
||||
}
|
||||
|
||||
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) => {
|
||||
info!("Stopping internal bitcoind...");
|
||||
bitcoind.stop();
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use liana::config::BitcoinBackend;
|
||||
use lianad::config::BitcoinBackend;
|
||||
|
||||
pub mod bitcoind;
|
||||
pub mod electrum;
|
||||
|
||||
@ -6,19 +6,7 @@ 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 = []
|
||||
description = "Liana development kit"
|
||||
|
||||
[dependencies]
|
||||
# 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.
|
||||
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
|
||||
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 }
|
||||
|
||||
# Used for generating mnemonics
|
||||
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;
|
||||
mod jsonrpc;
|
||||
pub mod random;
|
||||
pub mod signer;
|
||||
pub mod spend;
|
||||
#[cfg(test)]
|
||||
mod testutils;
|
||||
|
||||
pub use bdk_electrum::electrum_client;
|
||||
pub use bip39;
|
||||
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},
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{descriptors, testutils::*};
|
||||
use crate::descriptors;
|
||||
use miniscript::{
|
||||
bitcoin::{locktime::absolute, psbt::Input as PsbtIn, Amount},
|
||||
descriptor::{DerivPaths, DescriptorMultiXKey, DescriptorPublicKey, Wildcard},
|
||||
};
|
||||
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]
|
||||
fn hot_signer_gen() {
|
||||
// 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"))]
|
||||
|
||||
use liana::config::{config_folder_path, Config};
|
||||
use lianad::config::{config_folder_path, Config};
|
||||
|
||||
use std::{
|
||||
env,
|
||||
@ -5,7 +5,7 @@ use std::{
|
||||
process, thread, time,
|
||||
};
|
||||
|
||||
use liana::{config::Config, DaemonHandle, VERSION};
|
||||
use lianad::{config::Config, DaemonHandle, VERSION};
|
||||
|
||||
fn print_help_exit(code: i32) {
|
||||
eprintln!("lianad version {}", VERSION);
|
||||
@ -6,8 +6,8 @@ mod utils;
|
||||
use crate::{
|
||||
bitcoin::{Block, BlockChainTip},
|
||||
config,
|
||||
descriptors::LianaDescriptor,
|
||||
};
|
||||
use liana::descriptors::LianaDescriptor;
|
||||
use utils::{block_before_date, roundup_progress};
|
||||
|
||||
use std::{
|
||||
@ -17,10 +17,8 @@ use miniscript::bitcoin::bip32::ChildNumber;
|
||||
use super::utils::{
|
||||
block_id_from_tip, block_info_from_anchor, height_i32_from_u32, height_u32_from_i32,
|
||||
};
|
||||
use crate::{
|
||||
bitcoin::{Block, BlockChainTip, Coin, COINBASE_MATURITY},
|
||||
descriptors::LianaDescriptor,
|
||||
};
|
||||
use crate::bitcoin::{Block, BlockChainTip, Coin, COINBASE_MATURITY};
|
||||
use liana::descriptors::LianaDescriptor;
|
||||
|
||||
// We don't want to overload the server (each SPK is separate call).
|
||||
const LOOK_AHEAD_LIMIT: u32 = 30;
|
||||
@ -6,11 +6,9 @@ pub mod d;
|
||||
pub mod electrum;
|
||||
pub mod poller;
|
||||
|
||||
use crate::{
|
||||
bitcoin::d::{BitcoindError, CachedTxGetter, LSBlockEntry},
|
||||
descriptors,
|
||||
};
|
||||
use crate::bitcoin::d::{BitcoindError, CachedTxGetter, LSBlockEntry};
|
||||
pub use d::{MempoolEntry, MempoolEntryFees, SyncProgress};
|
||||
use liana::descriptors;
|
||||
|
||||
use std::{fmt, sync};
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
use crate::{
|
||||
bitcoin::{BitcoinInterface, BlockChainTip, UTxO, UTxOAddress},
|
||||
database::{Coin, DatabaseConnection, DatabaseInterface},
|
||||
descriptors,
|
||||
};
|
||||
|
||||
use std::{collections::HashSet, convert::TryInto, sync, thread, time};
|
||||
|
||||
use liana::descriptors;
|
||||
use miniscript::bitcoin::{self, secp256k1};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -1,6 +1,7 @@
|
||||
mod looper;
|
||||
|
||||
use crate::{bitcoin::BitcoinInterface, database::DatabaseInterface, descriptors};
|
||||
use crate::{bitcoin::BitcoinInterface, database::DatabaseInterface};
|
||||
use liana::descriptors;
|
||||
|
||||
use std::{
|
||||
sync::{self, mpsc},
|
||||
@ -7,18 +7,21 @@ mod utils;
|
||||
use crate::{
|
||||
bitcoin::BitcoinInterface,
|
||||
database::{Coin, DatabaseConnection, DatabaseInterface},
|
||||
descriptors,
|
||||
miniscript::bitcoin::absolute::LockTime,
|
||||
poller::PollerMessage,
|
||||
spend::{
|
||||
self, create_spend, AddrInfo, AncestorInfo, CandidateCoin, CreateSpendRes,
|
||||
SpendCreationError, SpendOutputAddress, SpendTxFees, TxGetter,
|
||||
},
|
||||
DaemonControl, VERSION,
|
||||
};
|
||||
|
||||
pub use crate::database::{CoinStatus, LabelItem};
|
||||
|
||||
use liana::{
|
||||
descriptors,
|
||||
spend::{
|
||||
self, create_spend, AddrInfo, AncestorInfo, CandidateCoin, CreateSpendRes,
|
||||
SpendCreationError, SpendOutputAddress, SpendTxFees, TxGetter,
|
||||
},
|
||||
};
|
||||
|
||||
use utils::{
|
||||
deser_addr_assume_checked, deser_amount_from_sats, deser_fromstr, deser_hex, ser_amount,
|
||||
ser_hex, ser_to_string,
|
||||
@ -1280,7 +1283,8 @@ pub struct CreateRecoveryResult {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{bitcoin::Block, database::BlockInfo, spend::InsaneFeeInfo, testutils::*};
|
||||
use crate::{bitcoin::Block, database::BlockInfo, testutils::*};
|
||||
use liana::spend::InsaneFeeInfo;
|
||||
|
||||
use bitcoin::{
|
||||
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};
|
||||
|
||||
@ -25,8 +25,8 @@ use crate::{
|
||||
},
|
||||
Coin, CoinStatus, LabelItem,
|
||||
},
|
||||
descriptors::LianaDescriptor,
|
||||
};
|
||||
use liana::descriptors::LianaDescriptor;
|
||||
|
||||
use std::{
|
||||
cmp,
|
||||
@ -1,4 +1,4 @@
|
||||
use crate::descriptors::LianaDescriptor;
|
||||
use liana::descriptors::LianaDescriptor;
|
||||
|
||||
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::{
|
||||
BlockInfo, Coin, CoinStatus, DatabaseConnection, DatabaseInterface, LabelItem, Wallet,
|
||||
},
|
||||
descriptors, DaemonControl, DaemonHandle,
|
||||
DaemonControl, DaemonHandle,
|
||||
};
|
||||
use liana::descriptors;
|
||||
|
||||
use std::convert::TryInto;
|
||||
use std::{
|
||||
Loading…
x
Reference in New Issue
Block a user