Merge #1469: liana: remove daemon feature
c14c55beeea56655096eb873ba0bce4d37ccd6bb remove daemonize from config (edouardparis)
c8112ab02752f3f49e67452cc2ef5e628d6804ac move rpc code to its own module (edouardparis)
a636cda2f7f9925d44f7bde3069255b3cbc618ea liana: remove the daemon feature (edouardparis)
Pull request description:
We expect user to use systemd service to manage a daemon or
to handle their process by themself.
The json-rpc server is exposed only for unix systems through
unix sockets, it is moved to its own module while the implementation
for windows is marked as TODO.
ACKs for top commit:
jp1ac4:
ACK c14c55beee.
Tree-SHA512: d4acafda399e9645cb5a80474a46290b6fac1e9695bb6ce04ce1eec7f68209ae67c70f858cd8f4d3ea5371818c2d5f68f9bcb3e4da349f5f86e53c0ec1123883
This commit is contained in:
commit
9c6eae9ee4
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2812,7 +2812,6 @@ dependencies = [
|
||||
"fern",
|
||||
"getrandom",
|
||||
"jsonrpc 0.17.0",
|
||||
"libc",
|
||||
"log",
|
||||
"miniscript",
|
||||
"rdrand",
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
# Whether to run the process as a UNIX daemon (double fork magic)
|
||||
daemon = false
|
||||
|
||||
# (Optional) Path to the folder where we should store the application data.
|
||||
# Defaults to `.lianad` in your home folder.
|
||||
data_dir = "/home/wizardsardine/.lianad"
|
||||
|
||||
@ -12,16 +12,12 @@ exclude = [".github/", ".cirrus.yml", "tests/", "test_data/", "contrib/", "pypr
|
||||
[[bin]]
|
||||
name = "lianad"
|
||||
path = "src/bin/daemon.rs"
|
||||
required-features = ["daemon"]
|
||||
|
||||
[[bin]]
|
||||
name = "liana-cli"
|
||||
path = "src/bin/cli.rs"
|
||||
required-features = ["daemon"]
|
||||
|
||||
[features]
|
||||
default = ["daemon"]
|
||||
daemon = ["libc"]
|
||||
nonblocking_shutdown = []
|
||||
|
||||
[dependencies]
|
||||
@ -59,9 +55,6 @@ rusqlite = { version = "0.30", features = ["bundled", "unlock_notify"] }
|
||||
# To talk to bitcoind
|
||||
jsonrpc = { version = "0.17", features = ["minreq_http"], default-features = false }
|
||||
|
||||
# Used for daemonization
|
||||
libc = { version = "0.2", optional = true }
|
||||
|
||||
# Used for generating mnemonics
|
||||
getrandom = "0.2"
|
||||
|
||||
|
||||
@ -80,12 +80,7 @@ fn main() {
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
let handle = DaemonHandle::start_default(
|
||||
config,
|
||||
#[cfg(all(unix, feature = "daemon"))]
|
||||
true,
|
||||
)
|
||||
.unwrap_or_else(|e| {
|
||||
let handle = DaemonHandle::start_default(config, cfg!(unix)).unwrap_or_else(|e| {
|
||||
log::error!("Error starting Liana daemon: {}", e);
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
@ -80,11 +80,6 @@ fn default_poll_interval() -> Duration {
|
||||
Duration::from_secs(30)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn default_daemon() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Bitcoin backend config.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub enum BitcoinBackend {
|
||||
@ -153,10 +148,6 @@ pub struct BitcoinConfig {
|
||||
pub struct Config {
|
||||
/// An optional custom data directory
|
||||
pub data_dir: Option<PathBuf>,
|
||||
/// Whether to daemonize the process
|
||||
#[cfg(unix)]
|
||||
#[serde(default = "default_daemon")]
|
||||
pub daemon: bool,
|
||||
/// What messages to log
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_fromstr",
|
||||
@ -303,7 +294,6 @@ mod tests {
|
||||
// A valid config
|
||||
let toml_str = r#"
|
||||
data_dir = "/home/wizardsardine/custom/folder/"
|
||||
daemon = false
|
||||
log_level = "debug"
|
||||
main_descriptor = "wsh(andor(pk([aabbccdd]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk([aabbccdd]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#dw4ulnrs"
|
||||
|
||||
@ -318,11 +308,9 @@ mod tests {
|
||||
toml::from_str::<Config>(&toml_str).expect("Deserializing toml_str");
|
||||
|
||||
// A valid, round-tripping, config
|
||||
#[cfg(unix)] // On non-UNIX there is no 'daemon' member.
|
||||
{
|
||||
let toml_str = r#"
|
||||
data_dir = '/home/wizardsardine/custom/folder/'
|
||||
daemon = false
|
||||
log_level = 'TRACE'
|
||||
main_descriptor = 'wsh(andor(pk([aabbccdd]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk([aabbccdd]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#dw4ulnrs'
|
||||
|
||||
@ -340,12 +328,9 @@ mod tests {
|
||||
}
|
||||
|
||||
// A valid, round-tripping, config for a Taproot descriptor.
|
||||
|
||||
#[cfg(unix)] // On non-UNIX there is no 'daemon' member.
|
||||
{
|
||||
let toml_str = r#"
|
||||
data_dir = '/home/wizardsardine/custom/folder/'
|
||||
daemon = false
|
||||
log_level = 'TRACE'
|
||||
main_descriptor = 'tr([abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*,and_v(v:pk([abcdef01]xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*),older(52560)))#0mt7e93c'
|
||||
|
||||
@ -363,11 +348,9 @@ mod tests {
|
||||
}
|
||||
|
||||
// A valid, round-tripping, config with `auth` instead of `cookie_path`
|
||||
#[cfg(unix)] // On non-UNIX there is no 'daemon' member.
|
||||
{
|
||||
let toml_str = r#"
|
||||
data_dir = '/home/wizardsardine/custom/folder/'
|
||||
daemon = false
|
||||
log_level = 'TRACE'
|
||||
main_descriptor = 'wsh(andor(pk([aabbccdd]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk([aabbccdd]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#dw4ulnrs'
|
||||
|
||||
@ -386,7 +369,6 @@ mod tests {
|
||||
|
||||
// Invalid desc checksum
|
||||
let toml_str = r#"
|
||||
daemon = false
|
||||
log_level = "trace"
|
||||
data_dir = "/home/wizardsardine/custom/folder/"
|
||||
|
||||
@ -405,7 +387,6 @@ mod tests {
|
||||
|
||||
// Not enough parameters: missing the Bitcoin network
|
||||
let toml_str = r#"
|
||||
daemon = false
|
||||
log_level = "trace"
|
||||
data_dir = "/home/wizardsardine/custom/folder/"
|
||||
|
||||
|
||||
@ -1,69 +0,0 @@
|
||||
use std::env::set_current_dir;
|
||||
use std::fs::File;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::prelude::*;
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::path::Path;
|
||||
|
||||
// This code was highly inspired from Frank Denis (@jedisct1) 'daemonize-simple' crate,
|
||||
// available at https://github.com/jedisct1/rust-daemonize-simple/blob/master/src/unix.rs .
|
||||
// MIT licensed according to https://github.com/jedisct1/rust-daemonize-simple/blob/master/Cargo.toml
|
||||
pub unsafe fn daemonize(
|
||||
chdir: &Path,
|
||||
log_file: &Path,
|
||||
pid_file: &Path,
|
||||
) -> Result<(), &'static str> {
|
||||
match libc::fork() {
|
||||
-1 => return Err("fork() failed"),
|
||||
0 => {}
|
||||
_ => {
|
||||
libc::_exit(0);
|
||||
}
|
||||
}
|
||||
libc::setsid();
|
||||
match libc::fork() {
|
||||
-1 => return Err("Second fork() failed"),
|
||||
0 => {}
|
||||
_ => {
|
||||
libc::_exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
let fd = OpenOptions::new()
|
||||
.read(true)
|
||||
.open("/dev/null")
|
||||
.map_err(|_| "Unable to open the stdin file")?;
|
||||
if libc::dup2(fd.as_raw_fd(), 0) == -1 {
|
||||
return Err("dup2(stdin) failed");
|
||||
}
|
||||
let fd = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(log_file)
|
||||
.map_err(|_| "Unable to open the stdout file")?;
|
||||
if libc::dup2(fd.as_raw_fd(), 1) == -1 {
|
||||
return Err("dup2(stdout) failed");
|
||||
}
|
||||
let fd = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(log_file)
|
||||
.map_err(|_| "Unable to open the stderr file")?;
|
||||
if libc::dup2(fd.as_raw_fd(), 2) == -1 {
|
||||
return Err("dup2(stderr) failed");
|
||||
}
|
||||
|
||||
let pid = match libc::getpid() {
|
||||
-1 => return Err("getpid() failed"),
|
||||
pid => pid,
|
||||
};
|
||||
let pid_str = format!("{}", pid);
|
||||
File::create(pid_file)
|
||||
.map_err(|_| "Creating the PID file failed")?
|
||||
.write_all(pid_str.as_bytes())
|
||||
.map_err(|_| "Writing to the PID file failed")?;
|
||||
|
||||
set_current_dir(chdir).map_err(|_| "chdir() failed")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
commands::{CoinStatus, LabelItem},
|
||||
jsonrpc::{Error, Params, Request, Response},
|
||||
jsonrpc::rpc::{Error, Params, Request, Response},
|
||||
DaemonControl,
|
||||
};
|
||||
|
||||
|
||||
@ -1,215 +1,5 @@
|
||||
#[cfg(unix)]
|
||||
mod api;
|
||||
#[cfg(unix)]
|
||||
pub mod rpc;
|
||||
pub mod server;
|
||||
|
||||
use crate::commands;
|
||||
|
||||
use std::{error, fmt};
|
||||
|
||||
use serde::{self, Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[serde(untagged)]
|
||||
pub enum Params {
|
||||
Array(Vec<serde_json::Value>),
|
||||
Map(serde_json::Map<String, serde_json::Value>),
|
||||
}
|
||||
|
||||
impl Params {
|
||||
/// Get the parameter supposed to be at a given index / of a given name.
|
||||
pub fn get<Q>(&self, index: usize, name: &Q) -> Option<&serde_json::Value>
|
||||
where
|
||||
String: std::borrow::Borrow<Q>,
|
||||
Q: ?Sized + Ord + Eq + std::hash::Hash,
|
||||
{
|
||||
match self {
|
||||
Params::Array(vec) => vec.get(index),
|
||||
Params::Map(map) => map.get(name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[serde(untagged)]
|
||||
pub enum ReqId {
|
||||
Num(u64),
|
||||
Str(String),
|
||||
}
|
||||
|
||||
/// A JSONRPC2 request. See https://www.jsonrpc.org/specification#request_object.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Request {
|
||||
/// Version. Must be "2.0".
|
||||
pub jsonrpc: String,
|
||||
/// Command name.
|
||||
pub method: String,
|
||||
/// Command parameters.
|
||||
pub params: Option<Params>,
|
||||
/// Request identifier.
|
||||
pub id: ReqId,
|
||||
}
|
||||
|
||||
/// A failure to broadcast a transaction to the P2P network.
|
||||
const BROADCAST_ERROR: i64 = 1_000;
|
||||
|
||||
/// JSONRPC2 error codes. See https://www.jsonrpc.org/specification#error_object.
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum ErrorCode {
|
||||
/// The method does not exist / is not available.
|
||||
MethodNotFound,
|
||||
/// Invalid method parameter(s).
|
||||
InvalidParams,
|
||||
/// Internal error while handling the command.
|
||||
InternalError,
|
||||
/// Reserved for implementation-defined server-errors.
|
||||
ServerError(i64),
|
||||
}
|
||||
|
||||
impl From<&ErrorCode> for i64 {
|
||||
fn from(code: &ErrorCode) -> i64 {
|
||||
match code {
|
||||
ErrorCode::MethodNotFound => -32601,
|
||||
ErrorCode::InvalidParams => -32602,
|
||||
ErrorCode::InternalError => -32603,
|
||||
ErrorCode::ServerError(code) => *code,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for ErrorCode {
|
||||
fn from(code: i64) -> ErrorCode {
|
||||
match code {
|
||||
-32601 => ErrorCode::MethodNotFound,
|
||||
-32602 => ErrorCode::InvalidParams,
|
||||
-32603 => ErrorCode::InternalError,
|
||||
code => ErrorCode::ServerError(code),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deserialize<'a> for ErrorCode {
|
||||
fn deserialize<D>(deserializer: D) -> Result<ErrorCode, D::Error>
|
||||
where
|
||||
D: Deserializer<'a>,
|
||||
{
|
||||
let code: i64 = Deserialize::deserialize(deserializer)?;
|
||||
Ok(code.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ErrorCode {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_i64(self.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// JSONRPC2 error response. See https://www.jsonrpc.org/specification#error_object.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Error {
|
||||
pub code: ErrorCode,
|
||||
pub message: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub data: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn new(code: ErrorCode, message: impl Into<String>) -> Error {
|
||||
Error {
|
||||
message: message.into(),
|
||||
code,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn method_not_found() -> Error {
|
||||
Error::new(ErrorCode::MethodNotFound, "Method not found")
|
||||
}
|
||||
|
||||
pub fn invalid_params(message: impl Into<String>) -> Error {
|
||||
Error::new(
|
||||
ErrorCode::InvalidParams,
|
||||
format!("Invalid params: {}", message.into()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let code: i64 = (&self.code).into();
|
||||
write!(f, "{}: {}", code, self.message)
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for Error {}
|
||||
|
||||
impl From<commands::CommandError> for Error {
|
||||
fn from(e: commands::CommandError) -> Error {
|
||||
match e {
|
||||
commands::CommandError::NoOutpointForSelfSend
|
||||
| commands::CommandError::UnknownOutpoint(..)
|
||||
| commands::CommandError::InvalidFeerate(..)
|
||||
| commands::CommandError::AlreadySpent(..)
|
||||
| commands::CommandError::ImmatureCoinbase(..)
|
||||
| commands::CommandError::Address(..)
|
||||
| commands::CommandError::SpendCreation(..)
|
||||
| commands::CommandError::InsufficientFunds(..)
|
||||
| commands::CommandError::UnknownSpend(..)
|
||||
| commands::CommandError::SpendFinalization(..)
|
||||
| commands::CommandError::InsaneRescanTimestamp(..)
|
||||
| commands::CommandError::AlreadyRescanning
|
||||
| commands::CommandError::InvalidDerivationIndex
|
||||
| commands::CommandError::RbfError(..)
|
||||
| commands::CommandError::EmptyFilterList
|
||||
| commands::CommandError::RecoveryNotAvailable => {
|
||||
Error::new(ErrorCode::InvalidParams, e.to_string())
|
||||
}
|
||||
commands::CommandError::RescanTrigger(..) => {
|
||||
Error::new(ErrorCode::InternalError, e.to_string())
|
||||
}
|
||||
commands::CommandError::TxBroadcast(_) => {
|
||||
Error::new(ErrorCode::ServerError(BROADCAST_ERROR), e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// JSONRPC2 response. See https://www.jsonrpc.org/specification#response_object.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Response {
|
||||
/// Version. Must be "2.0".
|
||||
jsonrpc: String,
|
||||
/// Required on success. Must not exist on error.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
result: Option<serde_json::Value>,
|
||||
/// Required on error. Must not exist on success.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
error: Option<Error>,
|
||||
/// Request identifier.
|
||||
id: ReqId,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
fn new(id: ReqId, result: Option<serde_json::Value>, error: Option<Error>) -> Response {
|
||||
Response {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
result,
|
||||
error,
|
||||
id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn success(id: ReqId, result: serde_json::Value) -> Response {
|
||||
Response::new(id, Some(result), None)
|
||||
}
|
||||
|
||||
pub fn error(id: ReqId, error: Error) -> Response {
|
||||
Response::new(id, None, Some(error))
|
||||
}
|
||||
}
|
||||
|
||||
212
liana/src/jsonrpc/rpc.rs
Normal file
212
liana/src/jsonrpc/rpc.rs
Normal file
@ -0,0 +1,212 @@
|
||||
use crate::commands;
|
||||
|
||||
use std::{error, fmt};
|
||||
|
||||
use serde::{self, Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[serde(untagged)]
|
||||
pub enum Params {
|
||||
Array(Vec<serde_json::Value>),
|
||||
Map(serde_json::Map<String, serde_json::Value>),
|
||||
}
|
||||
|
||||
impl Params {
|
||||
/// Get the parameter supposed to be at a given index / of a given name.
|
||||
pub fn get<Q>(&self, index: usize, name: &Q) -> Option<&serde_json::Value>
|
||||
where
|
||||
String: std::borrow::Borrow<Q>,
|
||||
Q: ?Sized + Ord + Eq + std::hash::Hash,
|
||||
{
|
||||
match self {
|
||||
Params::Array(vec) => vec.get(index),
|
||||
Params::Map(map) => map.get(name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[serde(untagged)]
|
||||
pub enum ReqId {
|
||||
Num(u64),
|
||||
Str(String),
|
||||
}
|
||||
|
||||
/// A JSONRPC2 request. See https://www.jsonrpc.org/specification#request_object.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Request {
|
||||
/// Version. Must be "2.0".
|
||||
pub jsonrpc: String,
|
||||
/// Command name.
|
||||
pub method: String,
|
||||
/// Command parameters.
|
||||
pub params: Option<Params>,
|
||||
/// Request identifier.
|
||||
pub id: ReqId,
|
||||
}
|
||||
|
||||
/// A failure to broadcast a transaction to the P2P network.
|
||||
const BROADCAST_ERROR: i64 = 1_000;
|
||||
|
||||
/// JSONRPC2 error codes. See https://www.jsonrpc.org/specification#error_object.
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum ErrorCode {
|
||||
/// The method does not exist / is not available.
|
||||
MethodNotFound,
|
||||
/// Invalid method parameter(s).
|
||||
InvalidParams,
|
||||
/// Internal error while handling the command.
|
||||
InternalError,
|
||||
/// Reserved for implementation-defined server-errors.
|
||||
ServerError(i64),
|
||||
}
|
||||
|
||||
impl From<&ErrorCode> for i64 {
|
||||
fn from(code: &ErrorCode) -> i64 {
|
||||
match code {
|
||||
ErrorCode::MethodNotFound => -32601,
|
||||
ErrorCode::InvalidParams => -32602,
|
||||
ErrorCode::InternalError => -32603,
|
||||
ErrorCode::ServerError(code) => *code,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for ErrorCode {
|
||||
fn from(code: i64) -> ErrorCode {
|
||||
match code {
|
||||
-32601 => ErrorCode::MethodNotFound,
|
||||
-32602 => ErrorCode::InvalidParams,
|
||||
-32603 => ErrorCode::InternalError,
|
||||
code => ErrorCode::ServerError(code),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deserialize<'a> for ErrorCode {
|
||||
fn deserialize<D>(deserializer: D) -> Result<ErrorCode, D::Error>
|
||||
where
|
||||
D: Deserializer<'a>,
|
||||
{
|
||||
let code: i64 = Deserialize::deserialize(deserializer)?;
|
||||
Ok(code.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ErrorCode {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_i64(self.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// JSONRPC2 error response. See https://www.jsonrpc.org/specification#error_object.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Error {
|
||||
pub code: ErrorCode,
|
||||
pub message: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub data: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn new(code: ErrorCode, message: impl Into<String>) -> Error {
|
||||
Error {
|
||||
message: message.into(),
|
||||
code,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn method_not_found() -> Error {
|
||||
Error::new(ErrorCode::MethodNotFound, "Method not found")
|
||||
}
|
||||
|
||||
pub fn invalid_params(message: impl Into<String>) -> Error {
|
||||
Error::new(
|
||||
ErrorCode::InvalidParams,
|
||||
format!("Invalid params: {}", message.into()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let code: i64 = (&self.code).into();
|
||||
write!(f, "{}: {}", code, self.message)
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for Error {}
|
||||
|
||||
impl From<commands::CommandError> for Error {
|
||||
fn from(e: commands::CommandError) -> Error {
|
||||
match e {
|
||||
commands::CommandError::NoOutpointForSelfSend
|
||||
| commands::CommandError::UnknownOutpoint(..)
|
||||
| commands::CommandError::InvalidFeerate(..)
|
||||
| commands::CommandError::AlreadySpent(..)
|
||||
| commands::CommandError::ImmatureCoinbase(..)
|
||||
| commands::CommandError::Address(..)
|
||||
| commands::CommandError::SpendCreation(..)
|
||||
| commands::CommandError::InsufficientFunds(..)
|
||||
| commands::CommandError::UnknownSpend(..)
|
||||
| commands::CommandError::SpendFinalization(..)
|
||||
| commands::CommandError::InsaneRescanTimestamp(..)
|
||||
| commands::CommandError::AlreadyRescanning
|
||||
| commands::CommandError::InvalidDerivationIndex
|
||||
| commands::CommandError::RbfError(..)
|
||||
| commands::CommandError::EmptyFilterList
|
||||
| commands::CommandError::RecoveryNotAvailable => {
|
||||
Error::new(ErrorCode::InvalidParams, e.to_string())
|
||||
}
|
||||
commands::CommandError::RescanTrigger(..) => {
|
||||
Error::new(ErrorCode::InternalError, e.to_string())
|
||||
}
|
||||
commands::CommandError::TxBroadcast(_) => {
|
||||
Error::new(ErrorCode::ServerError(BROADCAST_ERROR), e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// JSONRPC2 response. See https://www.jsonrpc.org/specification#response_object.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Response {
|
||||
/// Version. Must be "2.0".
|
||||
jsonrpc: String,
|
||||
/// Required on success. Must not exist on error.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
result: Option<serde_json::Value>,
|
||||
/// Required on error. Must not exist on success.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
error: Option<Error>,
|
||||
/// Request identifier.
|
||||
id: ReqId,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
fn new(id: ReqId, result: Option<serde_json::Value>, error: Option<Error>) -> Response {
|
||||
Response {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
result,
|
||||
error,
|
||||
id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn success(id: ReqId, result: serde_json::Value) -> Response {
|
||||
Response::new(id, Some(result), None)
|
||||
}
|
||||
|
||||
pub fn error(id: ReqId, error: Error) -> Response {
|
||||
Response::new(id, None, Some(error))
|
||||
}
|
||||
}
|
||||
31
liana/src/jsonrpc/server/mod.rs
Normal file
31
liana/src/jsonrpc/server/mod.rs
Normal file
@ -0,0 +1,31 @@
|
||||
#[cfg(unix)]
|
||||
mod unix;
|
||||
|
||||
use std::{
|
||||
io, path,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
|
||||
use crate::DaemonControl;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn run(
|
||||
socket_path: &path::Path,
|
||||
daemon_control: DaemonControl,
|
||||
shutdown: Arc<AtomicBool>,
|
||||
) -> Result<(), io::Error> {
|
||||
let listener = unix::rpcserver_setup(socket_path)?;
|
||||
log::info!("JSONRPC server started.");
|
||||
let res = unix::rpcserver_loop(listener, daemon_control, shutdown);
|
||||
log::info!("JSONRPC server stopped.");
|
||||
res
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn run(
|
||||
_socket_path: &path::Path,
|
||||
_daemon_control: DaemonControl,
|
||||
_shutdown: Arc<AtomicBool>,
|
||||
) -> Result<(), io::Error> {
|
||||
todo!("Implement a json rpc server over Named pipe");
|
||||
}
|
||||
@ -4,13 +4,16 @@
|
||||
//! JSONRPC2 requests on a Unix Domain Socket.
|
||||
|
||||
use crate::{
|
||||
jsonrpc::{api, Request, Response},
|
||||
jsonrpc::{
|
||||
api,
|
||||
rpc::{Request, Response},
|
||||
},
|
||||
DaemonControl,
|
||||
};
|
||||
|
||||
use std::{
|
||||
io,
|
||||
os::unix::net,
|
||||
fs, io,
|
||||
os::unix::{fs::PermissionsExt, net},
|
||||
path,
|
||||
sync::{self, atomic},
|
||||
thread, time,
|
||||
@ -194,24 +197,20 @@ fn bind(socket_path: &path::Path) -> Result<net::UnixListener, io::Error> {
|
||||
pub fn rpcserver_setup(socket_path: &path::Path) -> Result<net::UnixListener, io::Error> {
|
||||
log::debug!("Binding socket at {}", socket_path.display());
|
||||
// Create the socket with RW permissions only for the user
|
||||
#[cfg(not(test))]
|
||||
let old_umask = unsafe { libc::umask(0o177) };
|
||||
#[allow(clippy::all)]
|
||||
let listener = bind(socket_path);
|
||||
let listener = bind(socket_path)?;
|
||||
|
||||
#[cfg(not(test))]
|
||||
unsafe {
|
||||
libc::umask(old_umask);
|
||||
}
|
||||
// Set the permissions to RW for the user only
|
||||
let permissions = fs::Permissions::from_mode(0o600);
|
||||
fs::set_permissions(socket_path, permissions)?;
|
||||
|
||||
listener
|
||||
Ok(listener)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
jsonrpc::{Params, ReqId},
|
||||
jsonrpc::rpc::{Params, ReqId},
|
||||
testutils::*,
|
||||
};
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
mod bitcoin;
|
||||
pub mod commands;
|
||||
pub mod config;
|
||||
#[cfg(all(unix, feature = "daemon"))]
|
||||
mod daemonize;
|
||||
mod database;
|
||||
pub mod descriptors;
|
||||
#[cfg(all(unix, feature = "daemon"))]
|
||||
mod jsonrpc;
|
||||
pub mod random;
|
||||
pub mod signer;
|
||||
@ -22,8 +19,8 @@ pub use crate::bitcoin::{
|
||||
d::{BitcoinD, BitcoindError, WalletError},
|
||||
electrum::{Electrum, ElectrumError},
|
||||
};
|
||||
#[cfg(all(unix, feature = "daemon"))]
|
||||
use crate::jsonrpc::server::{rpcserver_loop, rpcserver_setup};
|
||||
|
||||
use crate::jsonrpc::server;
|
||||
use crate::{
|
||||
bitcoin::{poller, BitcoinInterface},
|
||||
config::Config,
|
||||
@ -103,8 +100,6 @@ pub enum StartupError {
|
||||
Database(SqliteDbError),
|
||||
Bitcoind(BitcoindError),
|
||||
Electrum(ElectrumError),
|
||||
#[cfg(unix)]
|
||||
Daemonization(&'static str),
|
||||
#[cfg(windows)]
|
||||
NoWatchonlyInDatadir,
|
||||
}
|
||||
@ -140,8 +135,6 @@ impl fmt::Display for StartupError {
|
||||
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(unix)]
|
||||
Self::Daemonization(e) => write!(f, "Error when daemonizing: '{}'.", e),
|
||||
#[cfg(windows)]
|
||||
Self::NoWatchonlyInDatadir => {
|
||||
write!(
|
||||
@ -411,7 +404,6 @@ pub enum DaemonHandle {
|
||||
poller_handle: thread::JoinHandle<()>,
|
||||
control: DaemonControl,
|
||||
},
|
||||
#[cfg(feature = "daemon")]
|
||||
Server {
|
||||
poller_sender: mpsc::SyncSender<poller::PollerMessage>,
|
||||
poller_handle: thread::JoinHandle<()>,
|
||||
@ -435,7 +427,7 @@ impl DaemonHandle {
|
||||
config: Config,
|
||||
bitcoin: Option<impl BitcoinInterface + 'static>,
|
||||
db: Option<impl DatabaseInterface + 'static>,
|
||||
#[cfg(all(unix, feature = "daemon"))] with_rpc_server: bool,
|
||||
with_rpc_server: bool,
|
||||
) -> Result<Self, StartupError> {
|
||||
#[cfg(not(test))]
|
||||
setup_panic_hook();
|
||||
@ -490,20 +482,6 @@ impl DaemonHandle {
|
||||
(None, None) => Err(StartupError::MissingBitcoinBackendConfig)?,
|
||||
};
|
||||
|
||||
// If we are on a UNIX system and they told us to daemonize, do it now.
|
||||
// NOTE: it's safe to daemonize now, as we don't carry any open DB connection
|
||||
// https://www.sqlite.org/howtocorrupt.html#_carrying_an_open_database_connection_across_a_fork_
|
||||
#[cfg(all(unix, feature = "daemon"))]
|
||||
if config.daemon {
|
||||
log::info!("Daemonizing");
|
||||
let log_file = data_dir.as_path().join("log");
|
||||
let pid_file = data_dir.as_path().join("lianad.pid");
|
||||
unsafe {
|
||||
daemonize::daemonize(&data_dir, &log_file, &pid_file)
|
||||
.map_err(StartupError::Daemonization)?;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 =
|
||||
@ -525,7 +503,6 @@ impl DaemonHandle {
|
||||
// structure or through the JSONRPC server we may setup below.
|
||||
let control = DaemonControl::new(config, bit, poller_sender.clone(), db, secp);
|
||||
|
||||
#[cfg(all(unix, feature = "daemon"))]
|
||||
if with_rpc_server {
|
||||
let rpcserver_shutdown = sync::Arc::from(sync::atomic::AtomicBool::from(false));
|
||||
let rpcserver_handle = thread::Builder::new()
|
||||
@ -535,11 +512,7 @@ impl DaemonHandle {
|
||||
move || {
|
||||
let mut rpc_socket = data_dir;
|
||||
rpc_socket.push("lianad_rpc");
|
||||
let listener = rpcserver_setup(&rpc_socket)?;
|
||||
log::info!("JSONRPC server started.");
|
||||
|
||||
rpcserver_loop(listener, control, shutdown)?;
|
||||
log::info!("JSONRPC server stopped.");
|
||||
server::run(&rpc_socket, control, shutdown)?;
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
@ -564,13 +537,12 @@ impl DaemonHandle {
|
||||
/// and SQLite).
|
||||
pub fn start_default(
|
||||
config: Config,
|
||||
#[cfg(all(unix, feature = "daemon"))] with_rpc_server: bool,
|
||||
with_rpc_server: bool,
|
||||
) -> Result<DaemonHandle, StartupError> {
|
||||
Self::start(
|
||||
config,
|
||||
Option::<BitcoinD>::None,
|
||||
Option::<SqliteDb>::None,
|
||||
#[cfg(all(unix, feature = "daemon"))]
|
||||
with_rpc_server,
|
||||
)
|
||||
}
|
||||
@ -583,7 +555,6 @@ impl DaemonHandle {
|
||||
Self::Controller {
|
||||
ref poller_handle, ..
|
||||
} => !poller_handle.is_finished(),
|
||||
#[cfg(feature = "daemon")]
|
||||
Self::Server {
|
||||
ref poller_handle,
|
||||
ref rpcserver_handle,
|
||||
@ -606,7 +577,6 @@ impl DaemonHandle {
|
||||
poller_handle.join().expect("Poller thread must not panic");
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(feature = "daemon")]
|
||||
Self::Server {
|
||||
poller_sender,
|
||||
poller_handle,
|
||||
@ -857,8 +827,6 @@ mod tests {
|
||||
bitcoin_config,
|
||||
bitcoin_backend: Some(config::BitcoinBackend::Bitcoind(bitcoind_config)),
|
||||
data_dir: Some(data_dir),
|
||||
#[cfg(unix)]
|
||||
daemon: false,
|
||||
log_level: log::LevelFilter::Debug,
|
||||
main_descriptor: desc,
|
||||
};
|
||||
@ -867,12 +835,7 @@ mod tests {
|
||||
let t = thread::spawn({
|
||||
let config = config.clone();
|
||||
move || {
|
||||
let handle = DaemonHandle::start_default(
|
||||
config,
|
||||
#[cfg(all(unix, feature = "daemon"))]
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
let handle = DaemonHandle::start_default(config, false).unwrap();
|
||||
handle.stop().unwrap();
|
||||
}
|
||||
});
|
||||
@ -892,12 +855,7 @@ mod tests {
|
||||
let t = thread::spawn({
|
||||
let config = config.clone();
|
||||
move || {
|
||||
let handle = DaemonHandle::start_default(
|
||||
config,
|
||||
#[cfg(all(unix, feature = "daemon"))]
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
let handle = DaemonHandle::start_default(config, false).unwrap();
|
||||
handle.stop().unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
@ -544,7 +544,7 @@ impl DummyLiana {
|
||||
pub fn _new(
|
||||
bitcoin_interface: impl BitcoinInterface + 'static,
|
||||
database: impl DatabaseInterface + 'static,
|
||||
#[cfg(all(unix, feature = "daemon"))] rpc_server: bool,
|
||||
rpc_server: bool,
|
||||
) -> DummyLiana {
|
||||
let tmp_dir = tmp_dir();
|
||||
fs::create_dir_all(&tmp_dir).unwrap();
|
||||
@ -569,20 +569,13 @@ impl DummyLiana {
|
||||
bitcoin_config,
|
||||
bitcoin_backend: None,
|
||||
data_dir: Some(data_dir),
|
||||
#[cfg(unix)]
|
||||
daemon: false,
|
||||
log_level: log::LevelFilter::Debug,
|
||||
main_descriptor: desc,
|
||||
};
|
||||
|
||||
let handle = DaemonHandle::start(
|
||||
config,
|
||||
Some(bitcoin_interface),
|
||||
Some(database),
|
||||
#[cfg(all(unix, feature = "daemon"))]
|
||||
rpc_server,
|
||||
)
|
||||
.unwrap();
|
||||
let handle =
|
||||
DaemonHandle::start(config, Some(bitcoin_interface), Some(database), rpc_server)
|
||||
.unwrap();
|
||||
DummyLiana { tmp_dir, handle }
|
||||
}
|
||||
|
||||
@ -591,16 +584,10 @@ impl DummyLiana {
|
||||
bitcoin_interface: impl BitcoinInterface + 'static,
|
||||
database: impl DatabaseInterface + 'static,
|
||||
) -> DummyLiana {
|
||||
Self::_new(
|
||||
bitcoin_interface,
|
||||
database,
|
||||
#[cfg(all(unix, feature = "daemon"))]
|
||||
false,
|
||||
)
|
||||
Self::_new(bitcoin_interface, database, false)
|
||||
}
|
||||
|
||||
/// Creates a new DummyLiana interface which also spins up an RPC server.
|
||||
#[cfg(all(unix, feature = "daemon"))]
|
||||
pub fn new_server(
|
||||
bitcoin_interface: impl BitcoinInterface + 'static,
|
||||
database: impl DatabaseInterface + 'static,
|
||||
@ -611,7 +598,6 @@ impl DummyLiana {
|
||||
pub fn control(&self) -> &DaemonControl {
|
||||
match self.handle {
|
||||
DaemonHandle::Controller { ref control, .. } => control,
|
||||
#[cfg(feature = "daemon")]
|
||||
DaemonHandle::Server { .. } => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,7 +50,6 @@ class Lianad(TailableProc):
|
||||
|
||||
with open(self.conf_file, "w") as f:
|
||||
f.write(f"data_dir = '{datadir}'\n")
|
||||
f.write("daemon = false\n")
|
||||
f.write(f"log_level = '{LOG_LEVEL}'\n")
|
||||
|
||||
f.write(f'main_descriptor = "{multi_desc}"\n')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user